Skip to main content

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