Skip to main content

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