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