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