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