input_pipeline/
pointer_display_scale_handler.rs

1// Copyright 2022 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 crate::input_handler::{InputHandlerStatus, UnhandledInputHandler};
6use crate::utils::Position;
7use crate::{input_device, metrics, mouse_binding};
8use anyhow::{format_err, Error};
9use async_trait::async_trait;
10use derivative::Derivative;
11use fuchsia_inspect::health::Reporter;
12use metrics_registry::*;
13use std::rc::Rc;
14
15// TODO(https://fxbug.dev/42172817) Add trackpad support
16#[derive(Derivative)]
17#[derivative(Debug, PartialEq)]
18pub struct PointerDisplayScaleHandler {
19    /// The amount by which motion will be scaled up. E.g., a `scale_factor`
20    /// of 2 means that all motion will be multiplied by 2.
21    scale_factor: f32,
22
23    /// The inventory of this handler's Inspect status.
24    pub inspect_status: InputHandlerStatus,
25
26    /// The metrics logger.
27    #[derivative(Debug = "ignore", PartialEq = "ignore")]
28    metrics_logger: metrics::MetricsLogger,
29}
30
31#[async_trait(?Send)]
32impl UnhandledInputHandler for PointerDisplayScaleHandler {
33    async fn handle_unhandled_input_event(
34        self: Rc<Self>,
35        unhandled_input_event: input_device::UnhandledInputEvent,
36    ) -> Vec<input_device::InputEvent> {
37        match unhandled_input_event.clone() {
38            input_device::UnhandledInputEvent {
39                device_event:
40                    input_device::InputDeviceEvent::Mouse(mouse_binding::MouseEvent {
41                        location:
42                            mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
43                                millimeters: raw_mm,
44                            }),
45                        wheel_delta_v,
46                        wheel_delta_h,
47                        // Only the `Move` phase carries non-zero motion.
48                        phase: phase @ mouse_binding::MousePhase::Move,
49                        affected_buttons,
50                        pressed_buttons,
51                        is_precision_scroll,
52                    }),
53                device_descriptor: device_descriptor @ input_device::InputDeviceDescriptor::Mouse(_),
54                event_time,
55                trace_id: _,
56            } => {
57                self.inspect_status
58                    .count_received_event(input_device::InputEvent::from(unhandled_input_event));
59                let scaled_mm = self.scale_motion(raw_mm);
60                let input_event = input_device::InputEvent {
61                    device_event: input_device::InputDeviceEvent::Mouse(
62                        mouse_binding::MouseEvent {
63                            location: mouse_binding::MouseLocation::Relative(
64                                mouse_binding::RelativeLocation { millimeters: scaled_mm },
65                            ),
66                            wheel_delta_v,
67                            wheel_delta_h,
68                            phase,
69                            affected_buttons,
70                            pressed_buttons,
71                            is_precision_scroll,
72                        },
73                    ),
74                    device_descriptor,
75                    event_time,
76                    handled: input_device::Handled::No,
77                    trace_id: None,
78                };
79                vec![input_event]
80            }
81            input_device::UnhandledInputEvent {
82                device_event:
83                    input_device::InputDeviceEvent::Mouse(mouse_binding::MouseEvent {
84                        location,
85                        wheel_delta_v,
86                        wheel_delta_h,
87                        phase: phase @ mouse_binding::MousePhase::Wheel,
88                        affected_buttons,
89                        pressed_buttons,
90                        is_precision_scroll,
91                    }),
92                device_descriptor: device_descriptor @ input_device::InputDeviceDescriptor::Mouse(_),
93                event_time,
94                trace_id: _,
95            } => {
96                self.inspect_status
97                    .count_received_event(input_device::InputEvent::from(unhandled_input_event));
98                let scaled_wheel_delta_v = self.scale_wheel_delta(wheel_delta_v);
99                let scaled_wheel_delta_h = self.scale_wheel_delta(wheel_delta_h);
100                let input_event = input_device::InputEvent {
101                    device_event: input_device::InputDeviceEvent::Mouse(
102                        mouse_binding::MouseEvent {
103                            location,
104                            wheel_delta_v: scaled_wheel_delta_v,
105                            wheel_delta_h: scaled_wheel_delta_h,
106                            phase,
107                            affected_buttons,
108                            pressed_buttons,
109                            is_precision_scroll,
110                        },
111                    ),
112                    device_descriptor,
113                    event_time,
114                    handled: input_device::Handled::No,
115                    trace_id: None,
116                };
117                vec![input_event]
118            }
119            _ => vec![input_device::InputEvent::from(unhandled_input_event)],
120        }
121    }
122
123    fn set_handler_healthy(self: std::rc::Rc<Self>) {
124        self.inspect_status.health_node.borrow_mut().set_ok();
125    }
126
127    fn set_handler_unhealthy(self: std::rc::Rc<Self>, msg: &str) {
128        self.inspect_status.health_node.borrow_mut().set_unhealthy(msg);
129    }
130}
131
132impl PointerDisplayScaleHandler {
133    /// Creates a new [`PointerMotionDisplayScaleHandler`].
134    ///
135    /// Returns
136    /// * `Ok(Rc<Self>)` if `scale_factor` is finite and >= 1.0, and
137    /// * `Err(Error)` otherwise.
138    pub fn new(
139        scale_factor: f32,
140        input_handlers_node: &fuchsia_inspect::Node,
141        metrics_logger: metrics::MetricsLogger,
142    ) -> Result<Rc<Self>, Error> {
143        log::debug!("scale_factor={}", scale_factor);
144        use std::num::FpCategory;
145        let inspect_status = InputHandlerStatus::new(
146            input_handlers_node,
147            "pointer_display_scale_handler",
148            /* generates_events */ false,
149        );
150        match scale_factor.classify() {
151            FpCategory::Nan | FpCategory::Infinite | FpCategory::Zero | FpCategory::Subnormal => {
152                Err(format_err!(
153                    "scale_factor {} is not a `Normal` floating-point value",
154                    scale_factor
155                ))
156            }
157            FpCategory::Normal => {
158                if scale_factor < 0.0 {
159                    Err(format_err!("Inverting motion is not supported"))
160                } else if scale_factor < 1.0 {
161                    Err(format_err!("Down-scaling motion is not supported"))
162                } else {
163                    Ok(Rc::new(Self { scale_factor, inspect_status, metrics_logger }))
164                }
165            }
166        }
167    }
168
169    /// Scales `motion`, using the configuration in `self`.
170    fn scale_motion(self: &Rc<Self>, motion: Position) -> Position {
171        motion * self.scale_factor
172    }
173
174    /// Scales `wheel_delta`, using the configuration in `self`.
175    fn scale_wheel_delta(
176        self: &Rc<Self>,
177        wheel_delta: Option<mouse_binding::WheelDelta>,
178    ) -> Option<mouse_binding::WheelDelta> {
179        match wheel_delta {
180            None => None,
181            Some(delta) => Some(mouse_binding::WheelDelta {
182                raw_data: delta.raw_data,
183                physical_pixel: match delta.physical_pixel {
184                    None => {
185                        // this should never reach as pointer_sensor_scale_handler should
186                        // fill this field.
187                        self.metrics_logger.log_error(
188                            InputPipelineErrorMetricDimensionEvent::PointerDisplayScaleNoPhysicalPixel,
189                            "physical_pixel is none",
190                        );
191                        None
192                    }
193                    Some(pixel) => Some(self.scale_factor * pixel),
194                },
195            }),
196        }
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203    use crate::input_handler::InputHandler;
204    use crate::testing_utilities;
205    use assert_matches::assert_matches;
206    use fuchsia_async as fasync;
207    use maplit::hashset;
208    use std::cell::Cell;
209    use std::collections::HashSet;
210    use std::ops::Add;
211    use test_case::test_case;
212
213    const COUNTS_PER_MM: f32 = 12.0;
214    const DEVICE_DESCRIPTOR: input_device::InputDeviceDescriptor =
215        input_device::InputDeviceDescriptor::Mouse(mouse_binding::MouseDeviceDescriptor {
216            device_id: 0,
217            absolute_x_range: None,
218            absolute_y_range: None,
219            wheel_v_range: None,
220            wheel_h_range: None,
221            buttons: None,
222            counts_per_mm: COUNTS_PER_MM as u32,
223        });
224
225    std::thread_local! {static NEXT_EVENT_TIME: Cell<i64> = Cell::new(0)}
226
227    fn make_unhandled_input_event(
228        mouse_event: mouse_binding::MouseEvent,
229    ) -> input_device::UnhandledInputEvent {
230        let event_time = NEXT_EVENT_TIME.with(|t| {
231            let old = t.get();
232            t.set(old + 1);
233            old
234        });
235        input_device::UnhandledInputEvent {
236            device_event: input_device::InputDeviceEvent::Mouse(mouse_event),
237            device_descriptor: DEVICE_DESCRIPTOR.clone(),
238            event_time: zx::MonotonicInstant::from_nanos(event_time),
239            trace_id: None,
240        }
241    }
242
243    #[test_case(f32::NAN          => matches Err(_); "yields err for NaN scale")]
244    #[test_case(f32::INFINITY     => matches Err(_); "yields err for pos infinite scale")]
245    #[test_case(f32::NEG_INFINITY => matches Err(_); "yields err for neg infinite scale")]
246    #[test_case(             -1.0 => matches Err(_); "yields err for neg scale")]
247    #[test_case(              0.0 => matches Err(_); "yields err for pos zero scale")]
248    #[test_case(             -0.0 => matches Err(_); "yields err for neg zero scale")]
249    #[test_case(              0.5 => matches Err(_); "yields err for downscale")]
250    #[test_case(              1.0 => matches Ok(_);  "yields handler for unit scale")]
251    #[test_case(              1.5 => matches Ok(_);  "yields handler for upscale")]
252    fn new(scale_factor: f32) -> Result<Rc<PointerDisplayScaleHandler>, Error> {
253        let inspector = fuchsia_inspect::Inspector::default();
254        let test_node = inspector.root().create_child("test_node");
255        PointerDisplayScaleHandler::new(scale_factor, &test_node, metrics::MetricsLogger::default())
256    }
257
258    #[fuchsia::test(allow_stalls = false)]
259    async fn applies_scale_mm() {
260        let inspector = fuchsia_inspect::Inspector::default();
261        let test_node = inspector.root().create_child("test_node");
262        let handler =
263            PointerDisplayScaleHandler::new(2.0, &test_node, metrics::MetricsLogger::default())
264                .expect("failed to make handler");
265        let input_event = make_unhandled_input_event(mouse_binding::MouseEvent {
266            location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
267                millimeters: Position { x: 1.5, y: 4.5 },
268            }),
269            wheel_delta_v: None,
270            wheel_delta_h: None,
271            phase: mouse_binding::MousePhase::Move,
272            affected_buttons: hashset! {},
273            pressed_buttons: hashset! {},
274            is_precision_scroll: None,
275        });
276        assert_matches!(
277            handler.clone().handle_unhandled_input_event(input_event).await.as_slice(),
278            [input_device::InputEvent {
279                device_event:
280                    input_device::InputDeviceEvent::Mouse(mouse_binding::MouseEvent {
281                        location:
282                            mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {millimeters: Position { x, y }}),
283                        ..
284                    }),
285                ..
286            }] if *x == 3.0  && *y == 9.0
287        );
288    }
289
290    #[test_case(
291        mouse_binding::MouseEvent {
292            location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
293                millimeters: Position {
294                    x: 1.5 / COUNTS_PER_MM,
295                    y: 4.5 / COUNTS_PER_MM },
296            }),
297            wheel_delta_v: None,
298            wheel_delta_h: None,
299            phase: mouse_binding::MousePhase::Move,
300            affected_buttons: hashset! {},
301            pressed_buttons: hashset! {},
302            is_precision_scroll: None,
303        }; "move event")]
304    #[test_case(
305        mouse_binding::MouseEvent {
306            location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
307                millimeters: Position::zero(),
308            }),
309            wheel_delta_v: Some(mouse_binding::WheelDelta {
310                raw_data: mouse_binding::RawWheelDelta::Ticks(1),
311                physical_pixel: Some(1.0),
312            }),
313            wheel_delta_h: None,
314            phase: mouse_binding::MousePhase::Wheel,
315            affected_buttons: hashset! {},
316            pressed_buttons: hashset! {},
317            is_precision_scroll: None,
318        }; "wheel event")]
319    #[fuchsia::test(allow_stalls = false)]
320    async fn does_not_consume(event: mouse_binding::MouseEvent) {
321        let inspector = fuchsia_inspect::Inspector::default();
322        let test_node = inspector.root().create_child("test_node");
323        let handler =
324            PointerDisplayScaleHandler::new(2.0, &test_node, metrics::MetricsLogger::default())
325                .expect("failed to make handler");
326        let input_event = make_unhandled_input_event(event);
327        assert_matches!(
328            handler.clone().handle_unhandled_input_event(input_event).await.as_slice(),
329            [input_device::InputEvent { handled: input_device::Handled::No, .. }]
330        );
331    }
332
333    #[test_case(hashset! {       }; "empty buttons")]
334    #[test_case(hashset! {      1}; "one button")]
335    #[test_case(hashset! {1, 2, 3}; "multiple buttons")]
336    #[fuchsia::test(allow_stalls = false)]
337    async fn preserves_buttons_move_event(input_buttons: HashSet<u8>) {
338        let inspector = fuchsia_inspect::Inspector::default();
339        let test_node = inspector.root().create_child("test_node");
340        let handler =
341            PointerDisplayScaleHandler::new(2.0, &test_node, metrics::MetricsLogger::default())
342                .expect("failed to make handler");
343        let input_event = make_unhandled_input_event(mouse_binding::MouseEvent {
344            location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
345                millimeters: Position { x: 1.5 / COUNTS_PER_MM, y: 4.5 / COUNTS_PER_MM },
346            }),
347            wheel_delta_v: None,
348            wheel_delta_h: None,
349            phase: mouse_binding::MousePhase::Move,
350            affected_buttons: input_buttons.clone(),
351            pressed_buttons: input_buttons.clone(),
352            is_precision_scroll: None,
353        });
354        assert_matches!(
355            handler.clone().handle_unhandled_input_event(input_event).await.as_slice(),
356            [input_device::InputEvent {
357                device_event:
358                    input_device::InputDeviceEvent::Mouse(mouse_binding::MouseEvent { affected_buttons, pressed_buttons, ..}),
359                ..
360            }] if *affected_buttons == input_buttons && *pressed_buttons == input_buttons
361        );
362    }
363
364    #[test_case(hashset! {       }; "empty buttons")]
365    #[test_case(hashset! {      1}; "one button")]
366    #[test_case(hashset! {1, 2, 3}; "multiple buttons")]
367    #[fuchsia::test(allow_stalls = false)]
368    async fn preserves_buttons_wheel_event(input_buttons: HashSet<u8>) {
369        let inspector = fuchsia_inspect::Inspector::default();
370        let test_node = inspector.root().create_child("test_node");
371        let handler =
372            PointerDisplayScaleHandler::new(2.0, &test_node, metrics::MetricsLogger::default())
373                .expect("failed to make handler");
374        let input_event = make_unhandled_input_event(mouse_binding::MouseEvent {
375            location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
376                millimeters: Position::zero(),
377            }),
378            wheel_delta_v: Some(mouse_binding::WheelDelta {
379                raw_data: mouse_binding::RawWheelDelta::Ticks(1),
380                physical_pixel: Some(1.0),
381            }),
382            wheel_delta_h: None,
383            phase: mouse_binding::MousePhase::Wheel,
384            affected_buttons: input_buttons.clone(),
385            pressed_buttons: input_buttons.clone(),
386            is_precision_scroll: None,
387        });
388        assert_matches!(
389            handler.clone().handle_unhandled_input_event(input_event).await.as_slice(),
390            [input_device::InputEvent {
391                device_event:
392                    input_device::InputDeviceEvent::Mouse(mouse_binding::MouseEvent { affected_buttons, pressed_buttons, ..}),
393                ..
394            }] if *affected_buttons == input_buttons && *pressed_buttons == input_buttons
395        );
396    }
397
398    #[test_case(
399        mouse_binding::MouseEvent {
400            location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
401                millimeters: Position {
402                    x: 1.5 / COUNTS_PER_MM,
403                    y: 4.5 / COUNTS_PER_MM },
404            }),
405            wheel_delta_v: None,
406            wheel_delta_h: None,
407            phase: mouse_binding::MousePhase::Move,
408            affected_buttons: hashset! {},
409            pressed_buttons: hashset! {},
410            is_precision_scroll: None,
411        }; "move event")]
412    #[test_case(
413        mouse_binding::MouseEvent {
414            location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
415                millimeters: Position::zero(),
416            }),
417            wheel_delta_v: Some(mouse_binding::WheelDelta {
418                raw_data: mouse_binding::RawWheelDelta::Ticks(1),
419                physical_pixel: Some(1.0),
420            }),
421            wheel_delta_h: None,
422            phase: mouse_binding::MousePhase::Wheel,
423            affected_buttons: hashset! {},
424            pressed_buttons: hashset! {},
425            is_precision_scroll: None,
426        }; "wheel event")]
427    #[fuchsia::test(allow_stalls = false)]
428    async fn preserves_descriptor(event: mouse_binding::MouseEvent) {
429        let inspector = fuchsia_inspect::Inspector::default();
430        let test_node = inspector.root().create_child("test_node");
431        let handler =
432            PointerDisplayScaleHandler::new(2.0, &test_node, metrics::MetricsLogger::default())
433                .expect("failed to make handler");
434        let input_event = make_unhandled_input_event(event);
435        assert_matches!(
436            handler.clone().handle_unhandled_input_event(input_event).await.as_slice(),
437            [input_device::InputEvent { device_descriptor: DEVICE_DESCRIPTOR, .. }]
438        );
439    }
440
441    #[test_case(
442        mouse_binding::MouseEvent {
443            location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
444                millimeters: Position {
445                    x: 1.5 / COUNTS_PER_MM,
446                    y: 4.5 / COUNTS_PER_MM },
447            }),
448            wheel_delta_v: None,
449            wheel_delta_h: None,
450            phase: mouse_binding::MousePhase::Move,
451            affected_buttons: hashset! {},
452            pressed_buttons: hashset! {},
453            is_precision_scroll: None,
454        }; "move event")]
455    #[test_case(
456        mouse_binding::MouseEvent {
457            location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
458                millimeters: Position::zero(),
459            }),
460            wheel_delta_v: Some(mouse_binding::WheelDelta {
461                raw_data: mouse_binding::RawWheelDelta::Ticks(1),
462                physical_pixel: Some(1.0),
463            }),
464            wheel_delta_h: None,
465            phase: mouse_binding::MousePhase::Wheel,
466            affected_buttons: hashset! {},
467            pressed_buttons: hashset! {},
468            is_precision_scroll: None,
469        }; "wheel event")]
470    #[fuchsia::test(allow_stalls = false)]
471    async fn preserves_event_time(event: mouse_binding::MouseEvent) {
472        let inspector = fuchsia_inspect::Inspector::default();
473        let test_node = inspector.root().create_child("test_node");
474        let handler =
475            PointerDisplayScaleHandler::new(2.0, &test_node, metrics::MetricsLogger::default())
476                .expect("failed to make handler");
477        let mut input_event = make_unhandled_input_event(event);
478        const EVENT_TIME: zx::MonotonicInstant = zx::MonotonicInstant::from_nanos(42);
479        input_event.event_time = EVENT_TIME;
480
481        let events = handler.clone().handle_unhandled_input_event(input_event).await;
482        assert_eq!(events.len(), 1, "{events:?} should be 1 element");
483        assert_eq!(events[0].event_time, EVENT_TIME);
484    }
485
486    #[test_case(
487        mouse_binding::MouseEvent {
488            location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
489                millimeters: Position::zero(),
490            }),
491            wheel_delta_v: Some(mouse_binding::WheelDelta {
492                raw_data: mouse_binding::RawWheelDelta::Ticks(1),
493                physical_pixel: Some(1.0),
494            }),
495            wheel_delta_h: None,
496            phase: mouse_binding::MousePhase::Wheel,
497            affected_buttons: hashset! {},
498            pressed_buttons: hashset! {},
499            is_precision_scroll: Some(mouse_binding::PrecisionScroll::No),
500        } => matches input_device::InputEvent {
501            device_event: input_device::InputDeviceEvent::Mouse(mouse_binding::MouseEvent {
502                is_precision_scroll: Some(mouse_binding::PrecisionScroll::No),
503                ..
504            }),
505            ..
506        }; "no")]
507    #[test_case(
508        mouse_binding::MouseEvent {
509            location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
510                millimeters: Position::zero(),
511            }),
512            wheel_delta_v: Some(mouse_binding::WheelDelta {
513                raw_data: mouse_binding::RawWheelDelta::Ticks(1),
514                physical_pixel: Some(1.0),
515            }),
516            wheel_delta_h: None,
517            phase: mouse_binding::MousePhase::Wheel,
518            affected_buttons: hashset! {},
519            pressed_buttons: hashset! {},
520            is_precision_scroll: Some(mouse_binding::PrecisionScroll::Yes),
521        } => matches input_device::InputEvent {
522            device_event: input_device::InputDeviceEvent::Mouse(mouse_binding::MouseEvent {
523                is_precision_scroll: Some(mouse_binding::PrecisionScroll::Yes),
524                ..
525            }),
526            ..
527        }; "yes")]
528    #[fuchsia::test(allow_stalls = false)]
529    async fn preserves_is_precision_scroll(
530        event: mouse_binding::MouseEvent,
531    ) -> input_device::InputEvent {
532        let inspector = fuchsia_inspect::Inspector::default();
533        let test_node = inspector.root().create_child("test_node");
534        let handler =
535            PointerDisplayScaleHandler::new(2.0, &test_node, metrics::MetricsLogger::default())
536                .expect("failed to make handler");
537        let input_event = make_unhandled_input_event(event);
538
539        handler.clone().handle_unhandled_input_event(input_event).await[0].clone()
540    }
541
542    #[test_case(
543        Some(mouse_binding::WheelDelta {
544            raw_data: mouse_binding::RawWheelDelta::Ticks(1),
545            physical_pixel: Some(1.0),
546        }),
547        None => (Some(2.0), None); "v tick h none"
548    )]
549    #[test_case(
550        None, Some(mouse_binding::WheelDelta {
551            raw_data: mouse_binding::RawWheelDelta::Ticks(1),
552            physical_pixel: Some(1.0),
553        })  => (None, Some(2.0)); "v none h tick"
554    )]
555    #[test_case(
556        Some(mouse_binding::WheelDelta {
557            raw_data: mouse_binding::RawWheelDelta::Millimeters(1.0),
558            physical_pixel: Some(1.0),
559        }),
560        None => (Some(2.0), None); "v mm h none"
561    )]
562    #[test_case(
563        None, Some(mouse_binding::WheelDelta {
564            raw_data: mouse_binding::RawWheelDelta::Millimeters(1.0),
565            physical_pixel: Some(1.0),
566        }) => (None, Some(2.0)); "v none h mm"
567    )]
568    #[fuchsia::test(allow_stalls = false)]
569    async fn applied_scale_scroll_event(
570        wheel_delta_v: Option<mouse_binding::WheelDelta>,
571        wheel_delta_h: Option<mouse_binding::WheelDelta>,
572    ) -> (Option<f32>, Option<f32>) {
573        let inspector = fuchsia_inspect::Inspector::default();
574        let test_node = inspector.root().create_child("test_node");
575        let handler =
576            PointerDisplayScaleHandler::new(2.0, &test_node, metrics::MetricsLogger::default())
577                .expect("failed to make handler");
578        let input_event = make_unhandled_input_event(mouse_binding::MouseEvent {
579            location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
580                millimeters: Position::zero(),
581            }),
582            wheel_delta_v,
583            wheel_delta_h,
584            phase: mouse_binding::MousePhase::Wheel,
585            affected_buttons: hashset! {},
586            pressed_buttons: hashset! {},
587            is_precision_scroll: None,
588        });
589        let events = handler.clone().handle_unhandled_input_event(input_event).await;
590        assert_matches!(
591            events.as_slice(),
592            [input_device::InputEvent {
593                device_event: input_device::InputDeviceEvent::Mouse(
594                    mouse_binding::MouseEvent { .. }
595                ),
596                ..
597            }]
598        );
599        if let input_device::InputEvent {
600            device_event:
601                input_device::InputDeviceEvent::Mouse(mouse_binding::MouseEvent {
602                    wheel_delta_v,
603                    wheel_delta_h,
604                    ..
605                }),
606            ..
607        } = events[0].clone()
608        {
609            match (wheel_delta_v, wheel_delta_h) {
610                (None, None) => return (None, None),
611                (None, Some(delta_h)) => return (None, delta_h.physical_pixel),
612                (Some(delta_v), None) => return (delta_v.physical_pixel, None),
613                (Some(delta_v), Some(delta_h)) => {
614                    return (delta_v.physical_pixel, delta_h.physical_pixel)
615                }
616            }
617        } else {
618            unreachable!();
619        }
620    }
621
622    #[fuchsia::test]
623    fn pointer_display_scale_handler_initialized_with_inspect_node() {
624        let inspector = fuchsia_inspect::Inspector::default();
625        let fake_handlers_node = inspector.root().create_child("input_handlers_node");
626        let _handler = PointerDisplayScaleHandler::new(
627            1.0,
628            &fake_handlers_node,
629            metrics::MetricsLogger::default(),
630        );
631        diagnostics_assertions::assert_data_tree!(inspector, root: {
632            input_handlers_node: {
633                pointer_display_scale_handler: {
634                    events_received_count: 0u64,
635                    events_handled_count: 0u64,
636                    last_received_timestamp_ns: 0u64,
637                    "fuchsia.inspect.Health": {
638                        status: "STARTING_UP",
639                        // Timestamp value is unpredictable and not relevant in this context,
640                        // so we only assert that the property is present.
641                        start_timestamp_nanos: diagnostics_assertions::AnyProperty
642                    },
643                }
644            }
645        });
646    }
647
648    #[fasync::run_singlethreaded(test)]
649    async fn pointer_display_scale_handler_inspect_counts_events() {
650        let inspector = fuchsia_inspect::Inspector::default();
651        let fake_handlers_node = inspector.root().create_child("input_handlers_node");
652        let handler = PointerDisplayScaleHandler::new(
653            1.0,
654            &fake_handlers_node,
655            metrics::MetricsLogger::default(),
656        )
657        .expect("failed to make handler");
658
659        let event_time1 = zx::MonotonicInstant::get();
660        let event_time2 = event_time1.add(zx::MonotonicDuration::from_micros(1));
661        let event_time3 = event_time2.add(zx::MonotonicDuration::from_micros(1));
662
663        let input_events = vec![
664            testing_utilities::create_mouse_event(
665                mouse_binding::MouseLocation::Absolute(Position { x: 0.0, y: 0.0 }),
666                None, /* wheel_delta_v */
667                None, /* wheel_delta_h */
668                None, /* is_precision_scroll */
669                mouse_binding::MousePhase::Wheel,
670                hashset! {},
671                hashset! {},
672                event_time1,
673                &DEVICE_DESCRIPTOR,
674            ),
675            testing_utilities::create_mouse_event(
676                mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
677                    millimeters: Position { x: 1.5 / COUNTS_PER_MM, y: 4.5 / COUNTS_PER_MM },
678                }),
679                None, /* wheel_delta_v */
680                None, /* wheel_delta_h */
681                None, /* is_precision_scroll */
682                mouse_binding::MousePhase::Move,
683                hashset! {},
684                hashset! {},
685                event_time2,
686                &DEVICE_DESCRIPTOR,
687            ),
688            // Should not count non-mouse input events.
689            testing_utilities::create_fake_input_event(event_time2),
690            // Should not count received events that have already been handled.
691            testing_utilities::create_mouse_event_with_handled(
692                mouse_binding::MouseLocation::Absolute(Position { x: 0.0, y: 0.0 }),
693                None, /* wheel_delta_v */
694                None, /* wheel_delta_h */
695                None, /* is_precision_scroll */
696                mouse_binding::MousePhase::Wheel,
697                hashset! {},
698                hashset! {},
699                event_time3,
700                &DEVICE_DESCRIPTOR,
701                input_device::Handled::Yes,
702            ),
703        ];
704
705        for input_event in input_events {
706            let _ = handler.clone().handle_input_event(input_event).await;
707        }
708
709        let last_received_event_time: u64 = event_time2.into_nanos().try_into().unwrap();
710
711        diagnostics_assertions::assert_data_tree!(inspector, root: {
712            input_handlers_node: {
713                pointer_display_scale_handler: {
714                    events_received_count: 2u64,
715                    events_handled_count: 0u64,
716                    last_received_timestamp_ns: last_received_event_time,
717                    "fuchsia.inspect.Health": {
718                        status: "STARTING_UP",
719                        // Timestamp value is unpredictable and not relevant in this context,
720                        // so we only assert that the property is present.
721                        start_timestamp_nanos: diagnostics_assertions::AnyProperty
722                    },
723                }
724            }
725        });
726    }
727}