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