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