Skip to main content

starnix_modules_input_event_conversion/
mouse_fuchsia_to_linux.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_ui_pointer::MouseEvent as FidlMouseEvent;
6use sorted_vec_map::SortedVecSet;
7use starnix_types::time::timeval_from_time;
8use starnix_uapi::uapi;
9use std::collections::VecDeque;
10
11#[derive(Clone, Debug, Default, PartialEq)]
12pub struct FuchsiaMouseEventToLinuxMouseEventConverter {
13    currently_pressed_buttons: SortedVecSet<u8>,
14}
15
16pub struct LinuxMouseEventBatch {
17    pub events: VecDeque<uapi::input_event>,
18    pub count_ignored_events: u64,
19    pub count_converted_events: u64,
20    pub count_unexpected_events: u64,
21    pub last_event_time_ns: i64,
22}
23
24/// Maps Fuchsia mouse button IDs to Linux input event key codes.
25///
26/// Returns None for unsupported or unrecognized button IDs so callers can
27/// gracefully ignore them.
28pub fn fuchsia_mouse_button_to_linux_button(button: u8) -> Option<u16> {
29    match button {
30        1 => Some(uapi::BTN_LEFT as u16),
31        2 => Some(uapi::BTN_RIGHT as u16),
32        3 => Some(uapi::BTN_MIDDLE as u16),
33        4 => Some(uapi::BTN_SIDE as u16),
34        5 => Some(uapi::BTN_EXTRA as u16),
35        _ => None,
36    }
37}
38
39impl FuchsiaMouseEventToLinuxMouseEventConverter {
40    pub fn create() -> Self {
41        Self::default()
42    }
43
44    /// Converts a batch of FIDL mouse events to Linux input events in a single pass.
45    ///
46    /// TODO(https://fxbug.dev/563345995): Batching was introduced as a performance
47    /// enhancement for scroll wheel events. Aggregating pointer and button events the
48    /// same way can have unintended effects: rapid click sequences may be collapsed,
49    /// and relative motion deltas that negate one another within a batch sum to zero
50    /// and are dropped entirely. Revisit before this path becomes load bearing.
51    pub fn handle(&mut self, mouse_events: Vec<FidlMouseEvent>) -> LinuxMouseEventBatch {
52        let mut count_ignored_events: u64 = 0;
53        let mut count_converted_events: u64 = 0;
54        let mut count_unexpected_events: u64 = 0;
55        let mut last_event_time = zx::MonotonicInstant::get();
56
57        let mut total_rel_x: i32 = 0;
58        let mut total_rel_y: i32 = 0;
59        let mut total_scroll_v: i32 = 0;
60        let mut total_scroll_h: i32 = 0;
61        let mut button_events: Vec<uapi::input_event> = Vec::new();
62
63        for event in mouse_events {
64            let Some(sample) = event.pointer_sample else {
65                if event.stream_info.is_some()
66                    || event.view_parameters.is_some()
67                    || event.device_info.is_some()
68                {
69                    count_ignored_events += 1;
70                } else {
71                    count_unexpected_events += 1;
72                }
73                continue;
74            };
75
76            let event_time = match event.timestamp {
77                Some(time) if time > 0 => zx::MonotonicInstant::from_nanos(time),
78                _ => zx::MonotonicInstant::get(),
79            };
80            last_event_time = event_time;
81            let time = timeval_from_time(event_time);
82
83            let mut sample_had_data = false;
84
85            // 1. Relative motion
86            //
87            // TODO(https://fxbug.dev/563345995): Rounding each sample independently
88            // discards sub-pixel motion (e.g. 0.4 becomes 0), which can make pointer
89            // tracking jittery or unresponsive at very low speeds. Accumulate the
90            // fractional remainder across samples on the converter, as standard
91            // pointer ballistics handling does.
92            if let Some([rx, ry]) = sample.relative_motion {
93                let dx = rx.round() as i32;
94                let dy = ry.round() as i32;
95                if dx != 0 {
96                    total_rel_x += dx;
97                    sample_had_data = true;
98                }
99                if dy != 0 {
100                    total_rel_y += dy;
101                    sample_had_data = true;
102                }
103            }
104
105            // 2. Scroll wheel (vertical and horizontal)
106            if let Some(ticks) = sample.scroll_v {
107                if ticks != 0 {
108                    total_scroll_v += ticks as i32;
109                    sample_had_data = true;
110                }
111            }
112            if let Some(ticks) = sample.scroll_h {
113                if ticks != 0 {
114                    total_scroll_h += ticks as i32;
115                    sample_had_data = true;
116                }
117            }
118
119            // 3. Buttons (transitions)
120            let new_pressed_buttons: SortedVecSet<u8> =
121                sample.pressed_buttons.map(|vec| vec.into_iter().collect()).unwrap_or_default();
122
123            for &btn in new_pressed_buttons.difference(&self.currently_pressed_buttons) {
124                if let Some(code) = fuchsia_mouse_button_to_linux_button(btn) {
125                    button_events.push(uapi::input_event {
126                        time,
127                        type_: uapi::EV_KEY as u16,
128                        code,
129                        value: 1,
130                    });
131                    sample_had_data = true;
132                }
133            }
134
135            for &btn in self.currently_pressed_buttons.difference(&new_pressed_buttons) {
136                if let Some(code) = fuchsia_mouse_button_to_linux_button(btn) {
137                    button_events.push(uapi::input_event {
138                        time,
139                        type_: uapi::EV_KEY as u16,
140                        code,
141                        value: 0,
142                    });
143                    sample_had_data = true;
144                }
145            }
146
147            self.currently_pressed_buttons = new_pressed_buttons;
148
149            if sample_had_data {
150                count_converted_events += 1;
151            } else {
152                count_ignored_events += 1;
153            }
154        }
155
156        let time = timeval_from_time(last_event_time);
157        let mut new_events: VecDeque<uapi::input_event> = VecDeque::new();
158
159        if total_rel_x != 0 {
160            new_events.push_back(uapi::input_event {
161                time,
162                type_: uapi::EV_REL as u16,
163                code: uapi::REL_X as u16,
164                value: total_rel_x,
165            });
166        }
167        if total_rel_y != 0 {
168            new_events.push_back(uapi::input_event {
169                time,
170                type_: uapi::EV_REL as u16,
171                code: uapi::REL_Y as u16,
172                value: total_rel_y,
173            });
174        }
175        if total_scroll_v != 0 {
176            new_events.push_back(uapi::input_event {
177                time,
178                type_: uapi::EV_REL as u16,
179                code: uapi::REL_WHEEL as u16,
180                value: total_scroll_v,
181            });
182        }
183        if total_scroll_h != 0 {
184            new_events.push_back(uapi::input_event {
185                time,
186                type_: uapi::EV_REL as u16,
187                code: uapi::REL_HWHEEL as u16,
188                value: total_scroll_h,
189            });
190        }
191        new_events.extend(button_events);
192
193        if !new_events.is_empty() {
194            new_events.push_back(uapi::input_event {
195                time,
196                type_: uapi::EV_SYN as u16,
197                code: uapi::SYN_REPORT as u16,
198                value: 0,
199            });
200        }
201
202        LinuxMouseEventBatch {
203            events: new_events,
204            count_ignored_events,
205            count_converted_events,
206            count_unexpected_events,
207            last_event_time_ns: last_event_time.into_nanos(),
208        }
209    }
210}
211
212pub fn parse_fidl_mouse_events(mouse_events: Vec<FidlMouseEvent>) -> LinuxMouseEventBatch {
213    FuchsiaMouseEventToLinuxMouseEventConverter::create().handle(mouse_events)
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219    use fidl_fuchsia_ui_pointer::{MouseEventStreamInfo, MousePointerSample, MouseViewStatus};
220    use pretty_assertions::assert_eq;
221
222    #[test]
223    fn test_mouse_wheel_event() {
224        let fidl_event = FidlMouseEvent {
225            timestamp: Some(1000),
226            pointer_sample: Some(MousePointerSample { scroll_v: Some(1), ..Default::default() }),
227            ..Default::default()
228        };
229        let batch = parse_fidl_mouse_events(vec![fidl_event]);
230
231        assert_eq!(batch.events.len(), 2);
232        assert_eq!(batch.events[0].type_, uapi::EV_REL as u16);
233        assert_eq!(batch.events[0].code, uapi::REL_WHEEL as u16);
234        assert_eq!(batch.events[0].value, 1);
235        assert_eq!(batch.events[1].type_, uapi::EV_SYN as u16);
236        assert_eq!(batch.events[1].code, uapi::SYN_REPORT as u16);
237        assert_eq!(batch.count_converted_events, 1);
238        assert_eq!(batch.count_ignored_events, 0);
239        assert_eq!(batch.count_unexpected_events, 0);
240        assert_eq!(batch.last_event_time_ns, 1000);
241    }
242
243    #[test]
244    fn test_mouse_horizontal_wheel_event() {
245        let fidl_event = FidlMouseEvent {
246            timestamp: Some(1000),
247            pointer_sample: Some(MousePointerSample { scroll_h: Some(3), ..Default::default() }),
248            ..Default::default()
249        };
250        let batch = parse_fidl_mouse_events(vec![fidl_event]);
251
252        assert_eq!(batch.events.len(), 2);
253        assert_eq!(batch.events[0].type_, uapi::EV_REL as u16);
254        assert_eq!(batch.events[0].code, uapi::REL_HWHEEL as u16);
255        assert_eq!(batch.events[0].value, 3);
256        assert_eq!(batch.events[1].type_, uapi::EV_SYN as u16);
257        assert_eq!(batch.events[1].code, uapi::SYN_REPORT as u16);
258        assert_eq!(batch.count_converted_events, 1);
259        assert_eq!(batch.count_ignored_events, 0);
260        assert_eq!(batch.count_unexpected_events, 0);
261        assert_eq!(batch.last_event_time_ns, 1000);
262    }
263
264    #[test]
265    fn test_mouse_wheel_merge() {
266        let fidl_event1 = FidlMouseEvent {
267            timestamp: Some(1000),
268            pointer_sample: Some(MousePointerSample { scroll_v: Some(1), ..Default::default() }),
269            ..Default::default()
270        };
271        let fidl_event2 = FidlMouseEvent {
272            timestamp: Some(2000),
273            pointer_sample: Some(MousePointerSample { scroll_v: Some(2), ..Default::default() }),
274            ..Default::default()
275        };
276        let batch = parse_fidl_mouse_events(vec![fidl_event1, fidl_event2]);
277
278        assert_eq!(batch.events.len(), 2);
279        assert_eq!(batch.events[0].type_, uapi::EV_REL as u16);
280        assert_eq!(batch.events[0].code, uapi::REL_WHEEL as u16);
281        assert_eq!(batch.events[0].value, 3);
282        assert_eq!(batch.events[1].type_, uapi::EV_SYN as u16);
283        assert_eq!(batch.events[1].code, uapi::SYN_REPORT as u16);
284        assert_eq!(batch.count_converted_events, 2);
285        assert_eq!(batch.count_ignored_events, 0);
286        assert_eq!(batch.count_unexpected_events, 0);
287        assert_eq!(batch.last_event_time_ns, 2000);
288    }
289
290    #[test]
291    fn test_mouse_wheel_merge_to_zero() {
292        let fidl_event1 = FidlMouseEvent {
293            timestamp: Some(1000),
294            pointer_sample: Some(MousePointerSample { scroll_v: Some(1), ..Default::default() }),
295            ..Default::default()
296        };
297        let fidl_event2 = FidlMouseEvent {
298            timestamp: Some(2000),
299            pointer_sample: Some(MousePointerSample { scroll_v: Some(-1), ..Default::default() }),
300            ..Default::default()
301        };
302        let batch = parse_fidl_mouse_events(vec![fidl_event1, fidl_event2]);
303
304        assert_eq!(batch.events.len(), 0);
305        assert_eq!(batch.count_converted_events, 2);
306        assert_eq!(batch.count_ignored_events, 0);
307        assert_eq!(batch.count_unexpected_events, 0);
308        assert_eq!(batch.last_event_time_ns, 2000);
309    }
310
311    #[test]
312    fn test_mouse_wheel_zero_ticks() {
313        let fidl_event = FidlMouseEvent {
314            timestamp: Some(1000),
315            pointer_sample: Some(MousePointerSample { scroll_v: Some(0), ..Default::default() }),
316            ..Default::default()
317        };
318        let batch = parse_fidl_mouse_events(vec![fidl_event]);
319
320        assert_eq!(batch.events.len(), 0);
321        assert_eq!(batch.count_converted_events, 0);
322        assert_eq!(batch.count_ignored_events, 1);
323        assert_eq!(batch.count_unexpected_events, 0);
324    }
325
326    #[test]
327    fn test_mouse_relative_motion() {
328        let fidl_event = FidlMouseEvent {
329            timestamp: Some(1000),
330            pointer_sample: Some(MousePointerSample {
331                relative_motion: Some([10.4, -5.2]),
332                ..Default::default()
333            }),
334            ..Default::default()
335        };
336        let batch = parse_fidl_mouse_events(vec![fidl_event]);
337
338        assert_eq!(batch.events.len(), 3);
339        assert_eq!(batch.events[0].type_, uapi::EV_REL as u16);
340        assert_eq!(batch.events[0].code, uapi::REL_X as u16);
341        assert_eq!(batch.events[0].value, 10);
342        assert_eq!(batch.events[1].type_, uapi::EV_REL as u16);
343        assert_eq!(batch.events[1].code, uapi::REL_Y as u16);
344        assert_eq!(batch.events[1].value, -5);
345        assert_eq!(batch.events[2].type_, uapi::EV_SYN as u16);
346        assert_eq!(batch.events[2].code, uapi::SYN_REPORT as u16);
347        assert_eq!(batch.count_converted_events, 1);
348        assert_eq!(batch.count_ignored_events, 0);
349        assert_eq!(batch.count_unexpected_events, 0);
350    }
351
352    #[test]
353    fn test_mouse_button_press_and_release() {
354        let mut converter = FuchsiaMouseEventToLinuxMouseEventConverter::create();
355
356        // 1. Press Left (1) and Right (2) buttons.
357        let press_event = FidlMouseEvent {
358            timestamp: Some(1000),
359            pointer_sample: Some(MousePointerSample {
360                pressed_buttons: Some(vec![1, 2]),
361                ..Default::default()
362            }),
363            ..Default::default()
364        };
365        let batch1 = converter.handle(vec![press_event]);
366        assert_eq!(batch1.events.len(), 3);
367        assert_eq!(batch1.events[0].type_, uapi::EV_KEY as u16);
368        assert_eq!(batch1.events[0].code, uapi::BTN_LEFT as u16);
369        assert_eq!(batch1.events[0].value, 1);
370        assert_eq!(batch1.events[1].type_, uapi::EV_KEY as u16);
371        assert_eq!(batch1.events[1].code, uapi::BTN_RIGHT as u16);
372        assert_eq!(batch1.events[1].value, 1);
373        assert_eq!(batch1.events[2].type_, uapi::EV_SYN as u16);
374        assert_eq!(batch1.events[2].code, uapi::SYN_REPORT as u16);
375        assert_eq!(batch1.count_converted_events, 1);
376
377        // 2. Release Left (1) button, keep Right (2) pressed.
378        let release_event = FidlMouseEvent {
379            timestamp: Some(2000),
380            pointer_sample: Some(MousePointerSample {
381                pressed_buttons: Some(vec![2]),
382                ..Default::default()
383            }),
384            ..Default::default()
385        };
386        let batch2 = converter.handle(vec![release_event]);
387        assert_eq!(batch2.events.len(), 2);
388        assert_eq!(batch2.events[0].type_, uapi::EV_KEY as u16);
389        assert_eq!(batch2.events[0].code, uapi::BTN_LEFT as u16);
390        assert_eq!(batch2.events[0].value, 0);
391        assert_eq!(batch2.events[1].type_, uapi::EV_SYN as u16);
392        assert_eq!(batch2.events[1].code, uapi::SYN_REPORT as u16);
393        assert_eq!(batch2.count_converted_events, 1);
394
395        // 3. Release Right (2) button.
396        let release_all = FidlMouseEvent {
397            timestamp: Some(3000),
398            pointer_sample: Some(MousePointerSample {
399                pressed_buttons: Some(vec![]),
400                ..Default::default()
401            }),
402            ..Default::default()
403        };
404        let batch3 = converter.handle(vec![release_all]);
405        assert_eq!(batch3.events.len(), 2);
406        assert_eq!(batch3.events[0].type_, uapi::EV_KEY as u16);
407        assert_eq!(batch3.events[0].code, uapi::BTN_RIGHT as u16);
408        assert_eq!(batch3.events[0].value, 0);
409        assert_eq!(batch3.events[1].type_, uapi::EV_SYN as u16);
410        assert_eq!(batch3.events[1].code, uapi::SYN_REPORT as u16);
411        assert_eq!(batch3.count_converted_events, 1);
412    }
413
414    #[test]
415    fn test_unsupported_mouse_button_ignored() {
416        let mut converter = FuchsiaMouseEventToLinuxMouseEventConverter::create();
417        let fidl_event = FidlMouseEvent {
418            timestamp: Some(1000),
419            pointer_sample: Some(MousePointerSample {
420                pressed_buttons: Some(vec![99]),
421                ..Default::default()
422            }),
423            ..Default::default()
424        };
425        let batch = converter.handle(vec![fidl_event]);
426        assert_eq!(batch.events.len(), 0);
427        assert_eq!(batch.count_converted_events, 0);
428        assert_eq!(batch.count_ignored_events, 1);
429    }
430
431    #[test]
432    fn test_zero_timestamp_uses_monotonic_instant() {
433        let fidl_event = FidlMouseEvent {
434            timestamp: Some(0),
435            pointer_sample: Some(MousePointerSample { scroll_v: Some(1), ..Default::default() }),
436            ..Default::default()
437        };
438        let batch = parse_fidl_mouse_events(vec![fidl_event]);
439        assert_eq!(batch.events.len(), 2);
440        assert_eq!(batch.events[0].code, uapi::REL_WHEEL as u16);
441        assert!(batch.events[0].time.tv_sec > 0);
442        assert!(batch.last_event_time_ns > 0);
443    }
444
445    #[test]
446    fn test_metadata_events_ignored() {
447        let stream_info_event = FidlMouseEvent {
448            stream_info: Some(MouseEventStreamInfo {
449                device_id: 1,
450                status: MouseViewStatus::Entered,
451            }),
452            ..Default::default()
453        };
454        let batch = parse_fidl_mouse_events(vec![stream_info_event]);
455        assert_eq!(batch.events.len(), 0);
456        assert_eq!(batch.count_ignored_events, 1);
457        assert_eq!(batch.count_unexpected_events, 0);
458    }
459
460    #[test]
461    fn test_mouse_unexpected_event() {
462        let fidl_event = FidlMouseEvent { timestamp: Some(1000), ..Default::default() };
463        let batch = parse_fidl_mouse_events(vec![fidl_event]);
464
465        assert_eq!(batch.events.len(), 0);
466        assert_eq!(batch.count_converted_events, 0);
467        assert_eq!(batch.count_ignored_events, 0);
468        assert_eq!(batch.count_unexpected_events, 1);
469    }
470}