input_pipeline/
pointer_sensor_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
5//!
6use crate::input_handler::{InputHandlerStatus, UnhandledInputHandler};
7use crate::utils::Position;
8use crate::{input_device, metrics, mouse_binding};
9use async_trait::async_trait;
10use fuchsia_inspect::health::Reporter;
11
12use metrics_registry::*;
13use std::cell::RefCell;
14use std::num::FpCategory;
15use std::rc::Rc;
16
17pub struct PointerSensorScaleHandler {
18    mutable_state: RefCell<MutableState>,
19
20    /// The inventory of this handler's Inspect status.
21    pub inspect_status: InputHandlerStatus,
22
23    /// The metrics logger.
24    metrics_logger: metrics::MetricsLogger,
25}
26
27struct MutableState {
28    /// The time of the last processed mouse move event.
29    last_move_timestamp: Option<zx::MonotonicInstant>,
30    /// The time of the last processed mouse scroll event.
31    last_scroll_timestamp: Option<zx::MonotonicInstant>,
32}
33
34/// For tick based scrolling, PointerSensorScaleHandler scales tick * 120 to logical
35/// pixel.
36const PIXELS_PER_TICK: f32 = 120.0;
37
38/// TODO(https://fxbug.dev/42059911): Temporary apply a linear scale factor to scroll to make it feel
39/// faster.
40const SCALE_SCROLL: f32 = 2.0;
41
42#[async_trait(?Send)]
43impl UnhandledInputHandler for PointerSensorScaleHandler {
44    async fn handle_unhandled_input_event(
45        self: Rc<Self>,
46        unhandled_input_event: input_device::UnhandledInputEvent,
47    ) -> Vec<input_device::InputEvent> {
48        match unhandled_input_event.clone() {
49            input_device::UnhandledInputEvent {
50                device_event:
51                    input_device::InputDeviceEvent::Mouse(mouse_binding::MouseEvent {
52                        location:
53                            mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
54                                millimeters: raw_motion,
55                            }),
56                        wheel_delta_v,
57                        wheel_delta_h,
58                        // Only the `Move` phase carries non-zero motion.
59                        phase: phase @ mouse_binding::MousePhase::Move,
60                        affected_buttons,
61                        pressed_buttons,
62                        is_precision_scroll,
63                    }),
64                device_descriptor:
65                    input_device::InputDeviceDescriptor::Mouse(mouse_binding::MouseDeviceDescriptor {
66                        absolute_x_range,
67                        absolute_y_range,
68                        buttons,
69                        counts_per_mm,
70                        device_id,
71                        wheel_h_range,
72                        wheel_v_range,
73                    }),
74                event_time,
75                trace_id,
76            } => {
77                let tracing_id = trace_id.unwrap_or_else(|| 0.into());
78                fuchsia_trace::duration!(c"input", c"pointer_sensor_scale_handler");
79                fuchsia_trace::flow_step!(c"input", c"event_in_input_pipeline", tracing_id);
80
81                self.inspect_status
82                    .count_received_event(input_device::InputEvent::from(unhandled_input_event));
83                let scaled_motion = self.scale_motion(raw_motion, event_time);
84                let input_event = input_device::InputEvent {
85                    device_event: input_device::InputDeviceEvent::Mouse(
86                        mouse_binding::MouseEvent {
87                            location: mouse_binding::MouseLocation::Relative(
88                                mouse_binding::RelativeLocation { millimeters: scaled_motion },
89                            ),
90                            wheel_delta_v,
91                            wheel_delta_h,
92                            phase,
93                            affected_buttons,
94                            pressed_buttons,
95                            is_precision_scroll,
96                        },
97                    ),
98                    device_descriptor: input_device::InputDeviceDescriptor::Mouse(
99                        mouse_binding::MouseDeviceDescriptor {
100                            absolute_x_range,
101                            absolute_y_range,
102                            buttons,
103                            counts_per_mm,
104                            device_id,
105                            wheel_h_range,
106                            wheel_v_range,
107                        },
108                    ),
109                    event_time,
110                    handled: input_device::Handled::No,
111                    trace_id,
112                };
113                vec![input_event]
114            }
115            input_device::UnhandledInputEvent {
116                device_event:
117                    input_device::InputDeviceEvent::Mouse(mouse_binding::MouseEvent {
118                        location,
119                        wheel_delta_v,
120                        wheel_delta_h,
121                        phase: phase @ mouse_binding::MousePhase::Wheel,
122                        affected_buttons,
123                        pressed_buttons,
124                        is_precision_scroll,
125                    }),
126                device_descriptor:
127                    input_device::InputDeviceDescriptor::Mouse(mouse_binding::MouseDeviceDescriptor {
128                        absolute_x_range,
129                        absolute_y_range,
130                        buttons,
131                        counts_per_mm,
132                        device_id,
133                        wheel_h_range,
134                        wheel_v_range,
135                    }),
136                event_time,
137                trace_id,
138            } => {
139                fuchsia_trace::duration!(c"input", c"pointer_sensor_scale_handler");
140                if let Some(trace_id) = trace_id {
141                    fuchsia_trace::flow_step!(
142                        c"input",
143                        c"event_in_input_pipeline",
144                        trace_id.into()
145                    );
146                }
147
148                self.inspect_status
149                    .count_received_event(input_device::InputEvent::from(unhandled_input_event));
150                let scaled_wheel_delta_v = self.scale_scroll(wheel_delta_v, event_time);
151                let scaled_wheel_delta_h = self.scale_scroll(wheel_delta_h, event_time);
152                let input_event = input_device::InputEvent {
153                    device_event: input_device::InputDeviceEvent::Mouse(
154                        mouse_binding::MouseEvent {
155                            location,
156                            wheel_delta_v: scaled_wheel_delta_v,
157                            wheel_delta_h: scaled_wheel_delta_h,
158                            phase,
159                            affected_buttons,
160                            pressed_buttons,
161                            is_precision_scroll,
162                        },
163                    ),
164                    device_descriptor: input_device::InputDeviceDescriptor::Mouse(
165                        mouse_binding::MouseDeviceDescriptor {
166                            absolute_x_range,
167                            absolute_y_range,
168                            buttons,
169                            counts_per_mm,
170                            device_id,
171                            wheel_h_range,
172                            wheel_v_range,
173                        },
174                    ),
175                    event_time,
176                    handled: input_device::Handled::No,
177                    trace_id,
178                };
179                vec![input_event]
180            }
181            _ => vec![input_device::InputEvent::from(unhandled_input_event)],
182        }
183    }
184
185    fn set_handler_healthy(self: std::rc::Rc<Self>) {
186        self.inspect_status.health_node.borrow_mut().set_ok();
187    }
188
189    fn set_handler_unhealthy(self: std::rc::Rc<Self>, msg: &str) {
190        self.inspect_status.health_node.borrow_mut().set_unhealthy(msg);
191    }
192}
193
194// The minimum reasonable delay between intentional mouse movements.
195// This value
196// * Is used to compensate for time compression if the driver gets
197//   backlogged.
198// * Is set to accommodate up to 10 kHZ event reporting.
199//
200// TODO(https://fxbug.dev/42181307): Use the polling rate instead of event timestamps.
201const MIN_PLAUSIBLE_EVENT_DELAY: zx::MonotonicDuration = zx::MonotonicDuration::from_micros(100);
202
203// The maximum reasonable delay between intentional mouse movements.
204// This value is used to compute speed for the first mouse motion after
205// a long idle period.
206//
207// Alternatively:
208// 1. The code could use the uncapped delay. However, this would lead to
209//    very slow initial motion after a long idle period.
210// 2. Wait until a second report comes in. However, older mice generate
211//    reports at 125 HZ, which would mean an 8 msec delay.
212//
213// TODO(https://fxbug.dev/42181307): Use the polling rate instead of event timestamps.
214const MAX_PLAUSIBLE_EVENT_DELAY: zx::MonotonicDuration = zx::MonotonicDuration::from_millis(50);
215
216const MAX_SENSOR_COUNTS_PER_INCH: f32 = 20_000.0; // From https://sensor.fyi/sensors
217const MAX_SENSOR_COUNTS_PER_MM: f32 = MAX_SENSOR_COUNTS_PER_INCH / 12.7;
218const MIN_MEASURABLE_DISTANCE_MM: f32 = 1.0 / MAX_SENSOR_COUNTS_PER_MM;
219const MAX_PLAUSIBLE_EVENT_DELAY_SECS: f32 = MAX_PLAUSIBLE_EVENT_DELAY.into_nanos() as f32 / 1E9;
220const MIN_MEASURABLE_VELOCITY_MM_PER_SEC: f32 =
221    MIN_MEASURABLE_DISTANCE_MM / MAX_PLAUSIBLE_EVENT_DELAY_SECS;
222
223// Define the buckets which determine which mapping to use.
224// * Speeds below the beginning of the medium range use the low-speed mapping.
225// * Speeds within the medium range use the medium-speed mapping.
226// * Speeds above the end of the medium range use the high-speed mapping.
227const MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC: f32 = 32.0;
228const MEDIUM_SPEED_RANGE_END_MM_PER_SEC: f32 = 150.0;
229
230// A linear factor affecting the responsiveness of the pointer to motion.
231// A higher numbness indicates lower responsiveness.
232const NUMBNESS: f32 = 37.5;
233
234impl PointerSensorScaleHandler {
235    /// Creates a new [`PointerSensorScaleHandler`].
236    ///
237    /// Returns `Rc<Self>`.
238    pub fn new(
239        input_handlers_node: &fuchsia_inspect::Node,
240        metrics_logger: metrics::MetricsLogger,
241    ) -> Rc<Self> {
242        let inspect_status = InputHandlerStatus::new(
243            input_handlers_node,
244            "pointer_sensor_scale_handler",
245            /* generates_events */ false,
246        );
247        Rc::new(Self {
248            mutable_state: RefCell::new(MutableState {
249                last_move_timestamp: None,
250                last_scroll_timestamp: None,
251            }),
252            inspect_status,
253            metrics_logger,
254        })
255    }
256
257    // Linearly scales `movement_mm_per_sec`.
258    //
259    // Given the values of `MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC` and
260    // `NUMBNESS` above, this results in downscaling the motion.
261    fn scale_low_speed(movement_mm_per_sec: f32) -> f32 {
262        const LINEAR_SCALE_FACTOR: f32 = MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC / NUMBNESS;
263        LINEAR_SCALE_FACTOR * movement_mm_per_sec
264    }
265
266    // Quadratically scales `movement_mm_per_sec`.
267    //
268    // The scale factor is chosen so that the composite curve is
269    // continuous as the speed transitions from the low-speed
270    // bucket to the medium-speed bucket.
271    //
272    // Note that the composite curve is _not_ differentiable at the
273    // transition from low-speed to medium-speed, since the
274    // slope on the left side of the point
275    // (MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC / NUMBNESS)
276    // is different from the slope on the right side of the point
277    // (2 * MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC / NUMBNESS).
278    //
279    // However, the transition works well enough in practice.
280    fn scale_medium_speed(movement_mm_per_sec: f32) -> f32 {
281        const QUARDRATIC_SCALE_FACTOR: f32 = 1.0 / NUMBNESS;
282        QUARDRATIC_SCALE_FACTOR * movement_mm_per_sec * movement_mm_per_sec
283    }
284
285    // Linearly scales `movement_mm_per_sec`.
286    //
287    // The parameters are chosen so that
288    // 1. The composite curve is continuous as the speed transitions
289    //    from the medium-speed bucket to the high-speed bucket.
290    // 2. The composite curve is differentiable.
291    fn scale_high_speed(movement_mm_per_sec: f32) -> f32 {
292        // Use linear scaling equal to the slope of `scale_medium_speed()`
293        // at the transition point.
294        const LINEAR_SCALE_FACTOR: f32 = 2.0 * (MEDIUM_SPEED_RANGE_END_MM_PER_SEC / NUMBNESS);
295
296        // Compute offset so the composite curve is continuous.
297        const Y_AT_MEDIUM_SPEED_RANGE_END_MM_PER_SEC: f32 =
298            MEDIUM_SPEED_RANGE_END_MM_PER_SEC * MEDIUM_SPEED_RANGE_END_MM_PER_SEC / NUMBNESS;
299        const OFFSET: f32 = Y_AT_MEDIUM_SPEED_RANGE_END_MM_PER_SEC
300            - LINEAR_SCALE_FACTOR * MEDIUM_SPEED_RANGE_END_MM_PER_SEC;
301
302        // Apply the computed transformation.
303        LINEAR_SCALE_FACTOR * movement_mm_per_sec + OFFSET
304    }
305
306    // Scales Euclidean velocity by one of the scale_*_speed_motion() functions above,
307    // choosing the function based on `MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC` and
308    // `MEDIUM_SPEED_RANGE_END_MM_PER_SEC`.
309    fn scale_euclidean_velocity(raw_velocity: f32) -> f32 {
310        if (0.0..MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC).contains(&raw_velocity) {
311            Self::scale_low_speed(raw_velocity)
312        } else if (MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC..MEDIUM_SPEED_RANGE_END_MM_PER_SEC)
313            .contains(&raw_velocity)
314        {
315            Self::scale_medium_speed(raw_velocity)
316        } else {
317            Self::scale_high_speed(raw_velocity)
318        }
319    }
320
321    /// Scales `movement_mm`.
322    fn scale_motion(&self, movement_mm: Position, event_time: zx::MonotonicInstant) -> Position {
323        // Determine the duration of this `movement`.
324        let elapsed_time_secs =
325            match self.mutable_state.borrow_mut().last_move_timestamp.replace(event_time) {
326                Some(last_event_time) => (event_time - last_event_time)
327                    .clamp(MIN_PLAUSIBLE_EVENT_DELAY, MAX_PLAUSIBLE_EVENT_DELAY),
328                None => MAX_PLAUSIBLE_EVENT_DELAY,
329            }
330            .into_nanos() as f32
331                / 1E9;
332
333        // Compute the velocity in each dimension.
334        let x_mm_per_sec = movement_mm.x / elapsed_time_secs;
335        let y_mm_per_sec = movement_mm.y / elapsed_time_secs;
336
337        let euclidean_velocity =
338            f32::sqrt(x_mm_per_sec * x_mm_per_sec + y_mm_per_sec * y_mm_per_sec);
339        if euclidean_velocity < MIN_MEASURABLE_VELOCITY_MM_PER_SEC {
340            // Avoid division by zero that would come from computing `scale_factor` below.
341            return movement_mm;
342        }
343
344        // Compute the scaling factor to be applied to each dimension.
345        //
346        // Geometrically, this is a bit dodgy when there's movement along both
347        // dimensions. Specifically: the `OFFSET` for high-speed motion should be
348        // constant, but the way its used here scales the offset based on velocity.
349        //
350        // Nonetheless, this works well enough in practice.
351        let scale_factor = Self::scale_euclidean_velocity(euclidean_velocity) / euclidean_velocity;
352
353        // Apply the scale factor and return the result.
354        let scaled_movement_mm = scale_factor * movement_mm;
355
356        match (scaled_movement_mm.x.classify(), scaled_movement_mm.y.classify()) {
357            (FpCategory::Infinite | FpCategory::Nan, _)
358            | (_, FpCategory::Infinite | FpCategory::Nan) => {
359                // Backstop, in case the code above missed some cases of bad arithmetic.
360                // Avoid sending `Infinite` or `Nan` values, since such values will
361                // poison the `current_position` in `MouseInjectorHandlerInner`.
362                // That manifests as the pointer becoming invisible, and never
363                // moving again.
364                //
365                // TODO(https://fxbug.dev/42181389) Add a triage rule to highlight the
366                // implications of this message.
367                self.metrics_logger.log_error(
368                    InputPipelineErrorMetricDimensionEvent::PointerSensorScaleHandlerScaledMotionInvalid,
369                    std::format!(
370                        "skipped motion; scaled movement of {:?} is infinite or NaN; x is {:?}, and y is {:?}",
371                        scaled_movement_mm,
372                        scaled_movement_mm.x.classify(),
373                        scaled_movement_mm.y.classify(),
374                ));
375                Position { x: 0.0, y: 0.0 }
376            }
377            _ => scaled_movement_mm,
378        }
379    }
380
381    /// `scroll_mm` scale with the curve algorithm.
382    /// `scroll_tick` scale with 120.
383    fn scale_scroll(
384        &self,
385        wheel_delta: Option<mouse_binding::WheelDelta>,
386        event_time: zx::MonotonicInstant,
387    ) -> Option<mouse_binding::WheelDelta> {
388        match wheel_delta {
389            None => None,
390            Some(mouse_binding::WheelDelta {
391                raw_data: mouse_binding::RawWheelDelta::Ticks(tick),
392                ..
393            }) => Some(mouse_binding::WheelDelta {
394                raw_data: mouse_binding::RawWheelDelta::Ticks(tick),
395                physical_pixel: Some(tick as f32 * PIXELS_PER_TICK),
396            }),
397            Some(mouse_binding::WheelDelta {
398                raw_data: mouse_binding::RawWheelDelta::Millimeters(mm),
399                ..
400            }) => {
401                // Determine the duration of this `scroll`.
402                let elapsed_time_secs =
403                    match self.mutable_state.borrow_mut().last_scroll_timestamp.replace(event_time)
404                    {
405                        Some(last_event_time) => (event_time - last_event_time)
406                            .clamp(MIN_PLAUSIBLE_EVENT_DELAY, MAX_PLAUSIBLE_EVENT_DELAY),
407                        None => MAX_PLAUSIBLE_EVENT_DELAY,
408                    }
409                    .into_nanos() as f32
410                        / 1E9;
411
412                let velocity = mm.abs() / elapsed_time_secs;
413
414                if velocity < MIN_MEASURABLE_VELOCITY_MM_PER_SEC {
415                    // Avoid division by zero that would come from computing
416                    // `scale_factor` below.
417                    return Some(mouse_binding::WheelDelta {
418                        raw_data: mouse_binding::RawWheelDelta::Millimeters(mm),
419                        physical_pixel: Some(SCALE_SCROLL * mm),
420                    });
421                }
422
423                let scale_factor = Self::scale_euclidean_velocity(velocity) / velocity;
424
425                // Apply the scale factor and return the result.
426                let scaled_scroll_mm = SCALE_SCROLL * scale_factor * mm;
427
428                if scaled_scroll_mm.is_infinite() || scaled_scroll_mm.is_nan() {
429                    self.metrics_logger.log_error(
430                        InputPipelineErrorMetricDimensionEvent::PointerSensorScaleHandlerScaledScrollInvalid,
431                        std::format!(
432                            "skipped scroll; scaled scroll of {:?} is infinite or NaN.",
433                            scaled_scroll_mm,
434                    ));
435                    return Some(mouse_binding::WheelDelta {
436                        raw_data: mouse_binding::RawWheelDelta::Millimeters(mm),
437                        physical_pixel: Some(SCALE_SCROLL * mm),
438                    });
439                }
440
441                Some(mouse_binding::WheelDelta {
442                    raw_data: mouse_binding::RawWheelDelta::Millimeters(mm),
443                    physical_pixel: Some(scaled_scroll_mm),
444                })
445            }
446        }
447    }
448}
449
450#[cfg(test)]
451mod tests {
452    use super::*;
453    use crate::input_handler::InputHandler;
454    use crate::testing_utilities;
455    use assert_matches::assert_matches;
456    use maplit::hashset;
457    use std::cell::Cell;
458    use std::ops::Add;
459    use test_util::{assert_gt, assert_lt, assert_near};
460    use {fuchsia_async as fasync, fuchsia_inspect};
461
462    const COUNTS_PER_MM: f32 = 12.0;
463    const DEVICE_DESCRIPTOR: input_device::InputDeviceDescriptor =
464        input_device::InputDeviceDescriptor::Mouse(mouse_binding::MouseDeviceDescriptor {
465            device_id: 0,
466            absolute_x_range: None,
467            absolute_y_range: None,
468            wheel_v_range: None,
469            wheel_h_range: None,
470            buttons: None,
471            counts_per_mm: COUNTS_PER_MM as u32,
472        });
473
474    // Maximum tolerable difference between "equal" scale factors. This is
475    // likely higher than FP rounding error can explain, but still small
476    // enough that there would be no user-perceptible difference.
477    //
478    // Rationale for not being user-perceptible: this requires the raw
479    // movement to have a count of 100,000, before there's a unit change
480    // in the scaled motion.
481    //
482    // On even the highest resolution sensor (per https://sensor.fyi/sensors),
483    // that would require 127mm (5 inches) of motion within one sampling
484    // interval.
485    //
486    // In the unlikely case that the high resolution sensor is paired
487    // with a low polling rate, that works out to 127mm/8msec, or _at least_
488    // 57 km/hr.
489    const SCALE_EPSILON: f32 = 1.0 / 100_000.0;
490
491    std::thread_local! {static NEXT_EVENT_TIME: Cell<i64> = Cell::new(0)}
492
493    fn make_unhandled_input_event(
494        mouse_event: mouse_binding::MouseEvent,
495    ) -> input_device::UnhandledInputEvent {
496        let event_time = NEXT_EVENT_TIME.with(|t| {
497            let old = t.get();
498            t.set(old + 1);
499            old
500        });
501        input_device::UnhandledInputEvent {
502            device_event: input_device::InputDeviceEvent::Mouse(mouse_event),
503            device_descriptor: DEVICE_DESCRIPTOR.clone(),
504            event_time: zx::MonotonicInstant::from_nanos(event_time),
505            trace_id: None,
506        }
507    }
508
509    #[fuchsia::test]
510    fn pointer_sensor_scale_handler_initialized_with_inspect_node() {
511        let inspector = fuchsia_inspect::Inspector::default();
512        let fake_handlers_node = inspector.root().create_child("input_handlers_node");
513        let _handler =
514            PointerSensorScaleHandler::new(&fake_handlers_node, metrics::MetricsLogger::default());
515        diagnostics_assertions::assert_data_tree!(inspector, root: {
516            input_handlers_node: {
517                pointer_sensor_scale_handler: {
518                    events_received_count: 0u64,
519                    events_handled_count: 0u64,
520                    last_received_timestamp_ns: 0u64,
521                    "fuchsia.inspect.Health": {
522                        status: "STARTING_UP",
523                        // Timestamp value is unpredictable and not relevant in this context,
524                        // so we only assert that the property is present.
525                        start_timestamp_nanos: diagnostics_assertions::AnyProperty
526                    },
527                }
528            }
529        });
530    }
531
532    #[fasync::run_singlethreaded(test)]
533    async fn pointer_sensor_scale_handler_inspect_counts_events() {
534        let inspector = fuchsia_inspect::Inspector::default();
535        let fake_handlers_node = inspector.root().create_child("input_handlers_node");
536        let handler =
537            PointerSensorScaleHandler::new(&fake_handlers_node, metrics::MetricsLogger::default());
538
539        let event_time1 = zx::MonotonicInstant::get();
540        let event_time2 = event_time1.add(zx::MonotonicDuration::from_micros(1));
541        let event_time3 = event_time2.add(zx::MonotonicDuration::from_micros(1));
542
543        let input_events = vec![
544            testing_utilities::create_mouse_event(
545                mouse_binding::MouseLocation::Absolute(Position { x: 0.0, y: 0.0 }),
546                None, /* wheel_delta_v */
547                None, /* wheel_delta_h */
548                None, /* is_precision_scroll */
549                mouse_binding::MousePhase::Wheel,
550                hashset! {},
551                hashset! {},
552                event_time1,
553                &DEVICE_DESCRIPTOR,
554            ),
555            testing_utilities::create_mouse_event(
556                mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
557                    millimeters: Position { x: 1.5 / COUNTS_PER_MM, y: 4.5 / COUNTS_PER_MM },
558                }),
559                None, /* wheel_delta_v */
560                None, /* wheel_delta_h */
561                None, /* is_precision_scroll */
562                mouse_binding::MousePhase::Move,
563                hashset! {},
564                hashset! {},
565                event_time2,
566                &DEVICE_DESCRIPTOR,
567            ),
568            // Should not count non-mouse input events.
569            testing_utilities::create_fake_input_event(event_time2),
570            // Should not count received events that have already been handled.
571            testing_utilities::create_mouse_event_with_handled(
572                mouse_binding::MouseLocation::Absolute(Position { x: 0.0, y: 0.0 }),
573                None, /* wheel_delta_v */
574                None, /* wheel_delta_h */
575                None, /* is_precision_scroll */
576                mouse_binding::MousePhase::Wheel,
577                hashset! {},
578                hashset! {},
579                event_time3,
580                &DEVICE_DESCRIPTOR,
581                input_device::Handled::Yes,
582            ),
583        ];
584
585        for input_event in input_events {
586            let _ = handler.clone().handle_input_event(input_event).await;
587        }
588
589        let last_received_event_time: u64 = event_time2.into_nanos().try_into().unwrap();
590
591        diagnostics_assertions::assert_data_tree!(inspector, root: {
592            input_handlers_node: {
593                pointer_sensor_scale_handler: {
594                    events_received_count: 2u64,
595                    events_handled_count: 0u64,
596                    last_received_timestamp_ns: last_received_event_time,
597                    "fuchsia.inspect.Health": {
598                        status: "STARTING_UP",
599                        // Timestamp value is unpredictable and not relevant in this context,
600                        // so we only assert that the property is present.
601                        start_timestamp_nanos: diagnostics_assertions::AnyProperty
602                    },
603                }
604            }
605        });
606    }
607
608    // While its generally preferred to write tests against the public API of
609    // a module, these tests
610    // 1. Can't be written against the public API (since that API doesn't
611    //    provide a way to control which curve is used for scaling), and
612    // 2. Validate important properties of the module.
613    mod internal_computations {
614        use super::*;
615
616        #[fuchsia::test]
617        fn transition_from_low_to_medium_is_continuous() {
618            assert_near!(
619                PointerSensorScaleHandler::scale_low_speed(MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC),
620                PointerSensorScaleHandler::scale_medium_speed(MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC),
621                SCALE_EPSILON
622            );
623        }
624
625        // As noted in `scale_motion()`, the offset will be applied imperfectly,
626        // so the externally visible transition may not be continuous.
627        //
628        // However, it's still valuable to verify that the internal building block
629        // works as intended.
630        #[fuchsia::test]
631        fn transition_from_medium_to_high_is_continuous() {
632            assert_near!(
633                PointerSensorScaleHandler::scale_medium_speed(MEDIUM_SPEED_RANGE_END_MM_PER_SEC),
634                PointerSensorScaleHandler::scale_high_speed(MEDIUM_SPEED_RANGE_END_MM_PER_SEC),
635                SCALE_EPSILON
636            );
637        }
638    }
639
640    mod motion_scaling_mm {
641        use super::*;
642
643        #[ignore]
644        #[fuchsia::test(allow_stalls = false)]
645        async fn plot_example_curve() {
646            let duration = zx::MonotonicDuration::from_millis(8);
647            for count in 1..1000 {
648                let scaled_count = get_scaled_motion_mm(
649                    Position { x: count as f32 / COUNTS_PER_MM, y: 0.0 },
650                    duration,
651                )
652                .await;
653                log::error!("{}, {}", count, scaled_count.x);
654            }
655        }
656
657        async fn get_scaled_motion_mm(
658            movement_mm: Position,
659            duration: zx::MonotonicDuration,
660        ) -> Position {
661            let inspector = fuchsia_inspect::Inspector::default();
662            let test_node = inspector.root().create_child("test_node");
663            let handler =
664                PointerSensorScaleHandler::new(&test_node, metrics::MetricsLogger::default());
665
666            // Send a don't-care value through to seed the last timestamp.
667            let input_event = input_device::UnhandledInputEvent {
668                device_event: input_device::InputDeviceEvent::Mouse(mouse_binding::MouseEvent {
669                    location: mouse_binding::MouseLocation::Relative(Default::default()),
670                    wheel_delta_v: None,
671                    wheel_delta_h: None,
672                    phase: mouse_binding::MousePhase::Move,
673                    affected_buttons: hashset! {},
674                    pressed_buttons: hashset! {},
675                    is_precision_scroll: None,
676                }),
677                device_descriptor: DEVICE_DESCRIPTOR.clone(),
678                event_time: zx::MonotonicInstant::from_nanos(0),
679                trace_id: None,
680            };
681            handler.clone().handle_unhandled_input_event(input_event).await;
682
683            // Send in the requested motion.
684            let input_event = input_device::UnhandledInputEvent {
685                device_event: input_device::InputDeviceEvent::Mouse(mouse_binding::MouseEvent {
686                    location: mouse_binding::MouseLocation::Relative(
687                        mouse_binding::RelativeLocation { millimeters: movement_mm },
688                    ),
689                    wheel_delta_v: None,
690                    wheel_delta_h: None,
691                    phase: mouse_binding::MousePhase::Move,
692                    affected_buttons: hashset! {},
693                    pressed_buttons: hashset! {},
694                    is_precision_scroll: None,
695                }),
696                device_descriptor: DEVICE_DESCRIPTOR.clone(),
697                event_time: zx::MonotonicInstant::from_nanos(duration.into_nanos()),
698                trace_id: None,
699            };
700            let transformed_events =
701                handler.clone().handle_unhandled_input_event(input_event).await;
702
703            // Provide a useful debug message if the transformed event doesn't have the expected
704            // overall structure.
705            assert_matches!(
706                transformed_events.as_slice(),
707                [input_device::InputEvent {
708                    device_event: input_device::InputDeviceEvent::Mouse(
709                        mouse_binding::MouseEvent {
710                            location: mouse_binding::MouseLocation::Relative(
711                                mouse_binding::RelativeLocation { .. }
712                            ),
713                            ..
714                        }
715                    ),
716                    ..
717                }]
718            );
719
720            // Return the transformed motion.
721            if let input_device::InputEvent {
722                device_event:
723                    input_device::InputDeviceEvent::Mouse(mouse_binding::MouseEvent {
724                        location:
725                            mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
726                                millimeters: movement_mm,
727                            }),
728                        ..
729                    }),
730                ..
731            } = transformed_events[0]
732            {
733                movement_mm
734            } else {
735                unreachable!()
736            }
737        }
738
739        fn velocity_to_mm(velocity_mm_per_sec: f32, duration: zx::MonotonicDuration) -> f32 {
740            velocity_mm_per_sec * (duration.into_nanos() as f32 / 1E9)
741        }
742
743        #[fuchsia::test(allow_stalls = false)]
744        async fn low_speed_horizontal_motion_scales_linearly() {
745            const TICK_DURATION: zx::MonotonicDuration = zx::MonotonicDuration::from_millis(8);
746            const MOTION_A_MM: f32 = 1.0 / COUNTS_PER_MM;
747            const MOTION_B_MM: f32 = 2.0 / COUNTS_PER_MM;
748            assert_lt!(
749                MOTION_B_MM,
750                velocity_to_mm(MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC, TICK_DURATION)
751            );
752
753            let scaled_a =
754                get_scaled_motion_mm(Position { x: MOTION_A_MM, y: 0.0 }, TICK_DURATION).await;
755            let scaled_b =
756                get_scaled_motion_mm(Position { x: MOTION_B_MM, y: 0.0 }, TICK_DURATION).await;
757            assert_near!(scaled_b.x / scaled_a.x, 2.0, SCALE_EPSILON);
758        }
759
760        #[fuchsia::test(allow_stalls = false)]
761        async fn low_speed_vertical_motion_scales_linearly() {
762            const TICK_DURATION: zx::MonotonicDuration = zx::MonotonicDuration::from_millis(8);
763            const MOTION_A_MM: f32 = 1.0 / COUNTS_PER_MM;
764            const MOTION_B_MM: f32 = 2.0 / COUNTS_PER_MM;
765            assert_lt!(
766                MOTION_B_MM,
767                velocity_to_mm(MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC, TICK_DURATION)
768            );
769
770            let scaled_a =
771                get_scaled_motion_mm(Position { x: 0.0, y: MOTION_A_MM }, TICK_DURATION).await;
772            let scaled_b =
773                get_scaled_motion_mm(Position { x: 0.0, y: MOTION_B_MM }, TICK_DURATION).await;
774            assert_near!(scaled_b.y / scaled_a.y, 2.0, SCALE_EPSILON);
775        }
776
777        #[fuchsia::test(allow_stalls = false)]
778        async fn low_speed_45degree_motion_scales_dimensions_equally() {
779            const TICK_DURATION: zx::MonotonicDuration = zx::MonotonicDuration::from_millis(8);
780            const MOTION_MM: f32 = 1.0 / COUNTS_PER_MM;
781            assert_lt!(
782                MOTION_MM,
783                velocity_to_mm(MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC, TICK_DURATION)
784            );
785
786            let scaled =
787                get_scaled_motion_mm(Position { x: MOTION_MM, y: MOTION_MM }, TICK_DURATION).await;
788            assert_near!(scaled.x, scaled.y, SCALE_EPSILON);
789        }
790
791        #[fuchsia::test(allow_stalls = false)]
792        async fn medium_speed_motion_scales_quadratically() {
793            const TICK_DURATION: zx::MonotonicDuration = zx::MonotonicDuration::from_millis(8);
794            const MOTION_A_MM: f32 = 7.0 / COUNTS_PER_MM;
795            const MOTION_B_MM: f32 = 14.0 / COUNTS_PER_MM;
796            assert_gt!(
797                MOTION_A_MM,
798                velocity_to_mm(MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC, TICK_DURATION)
799            );
800            assert_lt!(
801                MOTION_B_MM,
802                velocity_to_mm(MEDIUM_SPEED_RANGE_END_MM_PER_SEC, TICK_DURATION)
803            );
804
805            let scaled_a =
806                get_scaled_motion_mm(Position { x: MOTION_A_MM, y: 0.0 }, TICK_DURATION).await;
807            let scaled_b =
808                get_scaled_motion_mm(Position { x: MOTION_B_MM, y: 0.0 }, TICK_DURATION).await;
809            assert_near!(scaled_b.x / scaled_a.x, 4.0, SCALE_EPSILON);
810        }
811
812        // Given the handling of `OFFSET` for high-speed motion, (see comment
813        // in `scale_motion()`), high speed motion scaling is _not_ linear for
814        // the range of values of practical interest.
815        //
816        // Thus, this tests verifies a weaker property.
817        #[fuchsia::test(allow_stalls = false)]
818        async fn high_speed_motion_scaling_is_increasing() {
819            const TICK_DURATION: zx::MonotonicDuration = zx::MonotonicDuration::from_millis(8);
820            const MOTION_A_MM: f32 = 16.0 / COUNTS_PER_MM;
821            const MOTION_B_MM: f32 = 20.0 / COUNTS_PER_MM;
822            assert_gt!(
823                MOTION_A_MM,
824                velocity_to_mm(MEDIUM_SPEED_RANGE_END_MM_PER_SEC, TICK_DURATION)
825            );
826
827            let scaled_a =
828                get_scaled_motion_mm(Position { x: MOTION_A_MM, y: 0.0 }, TICK_DURATION).await;
829            let scaled_b =
830                get_scaled_motion_mm(Position { x: MOTION_B_MM, y: 0.0 }, TICK_DURATION).await;
831            assert_gt!(scaled_b.x, scaled_a.x)
832        }
833
834        #[fuchsia::test(allow_stalls = false)]
835        async fn zero_motion_maps_to_zero_motion() {
836            const TICK_DURATION: zx::MonotonicDuration = zx::MonotonicDuration::from_millis(8);
837            let scaled = get_scaled_motion_mm(Position { x: 0.0, y: 0.0 }, TICK_DURATION).await;
838            assert_eq!(scaled, Position::zero())
839        }
840
841        #[fuchsia::test(allow_stalls = false)]
842        async fn zero_duration_does_not_crash() {
843            get_scaled_motion_mm(
844                Position { x: 1.0 / COUNTS_PER_MM, y: 0.0 },
845                zx::MonotonicDuration::from_millis(0),
846            )
847            .await;
848        }
849    }
850
851    mod scroll_scaling_tick {
852        use super::*;
853        use test_case::test_case;
854
855        #[test_case(mouse_binding::MouseEvent {
856            location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
857                millimeters: Position::zero(),
858            }),
859            wheel_delta_v: Some(mouse_binding::WheelDelta {
860                raw_data: mouse_binding::RawWheelDelta::Ticks(1),
861                physical_pixel: None,
862            }),
863            wheel_delta_h: None,
864            phase: mouse_binding::MousePhase::Wheel,
865            affected_buttons: hashset! {},
866            pressed_buttons: hashset! {},
867            is_precision_scroll: None,
868        } => (Some(PIXELS_PER_TICK), None); "v")]
869        #[test_case(mouse_binding::MouseEvent {
870            location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
871                millimeters: Position::zero(),
872            }),
873            wheel_delta_v: None,
874            wheel_delta_h: Some(mouse_binding::WheelDelta {
875                raw_data: mouse_binding::RawWheelDelta::Ticks(1),
876                physical_pixel: None,
877            }),
878            phase: mouse_binding::MousePhase::Wheel,
879            affected_buttons: hashset! {},
880            pressed_buttons: hashset! {},
881            is_precision_scroll: None,
882        } => (None, Some(PIXELS_PER_TICK)); "h")]
883        #[fuchsia::test(allow_stalls = false)]
884        async fn scaled(event: mouse_binding::MouseEvent) -> (Option<f32>, Option<f32>) {
885            let inspector = fuchsia_inspect::Inspector::default();
886            let test_node = inspector.root().create_child("test_node");
887            let handler =
888                PointerSensorScaleHandler::new(&test_node, metrics::MetricsLogger::default());
889            let unhandled_event = make_unhandled_input_event(event);
890
891            let events = handler.clone().handle_unhandled_input_event(unhandled_event).await;
892            assert_matches!(
893                events.as_slice(),
894                [input_device::InputEvent {
895                    device_event: input_device::InputDeviceEvent::Mouse(
896                        mouse_binding::MouseEvent { .. }
897                    ),
898                    ..
899                }]
900            );
901            if let input_device::InputEvent {
902                device_event:
903                    input_device::InputDeviceEvent::Mouse(mouse_binding::MouseEvent {
904                        wheel_delta_v,
905                        wheel_delta_h,
906                        ..
907                    }),
908                ..
909            } = events[0].clone()
910            {
911                match (wheel_delta_v, wheel_delta_h) {
912                    (None, None) => return (None, None),
913                    (None, Some(delta_h)) => return (None, delta_h.physical_pixel),
914                    (Some(delta_v), None) => return (delta_v.physical_pixel, None),
915                    (Some(delta_v), Some(delta_h)) => {
916                        return (delta_v.physical_pixel, delta_h.physical_pixel)
917                    }
918                }
919            } else {
920                unreachable!();
921            }
922        }
923    }
924
925    mod scroll_scaling_mm {
926        use super::*;
927        use pretty_assertions::assert_eq;
928
929        async fn get_scaled_scroll_mm(
930            wheel_delta_v_mm: Option<f32>,
931            wheel_delta_h_mm: Option<f32>,
932            duration: zx::MonotonicDuration,
933        ) -> (Option<mouse_binding::WheelDelta>, Option<mouse_binding::WheelDelta>) {
934            let inspector = fuchsia_inspect::Inspector::default();
935            let test_node = inspector.root().create_child("test_node");
936            let handler =
937                PointerSensorScaleHandler::new(&test_node, metrics::MetricsLogger::default());
938
939            // Send a don't-care value through to seed the last timestamp.
940            let input_event = input_device::UnhandledInputEvent {
941                device_event: input_device::InputDeviceEvent::Mouse(mouse_binding::MouseEvent {
942                    location: mouse_binding::MouseLocation::Relative(Default::default()),
943                    wheel_delta_v: Some(mouse_binding::WheelDelta {
944                        raw_data: mouse_binding::RawWheelDelta::Millimeters(1.0),
945                        physical_pixel: None,
946                    }),
947                    wheel_delta_h: None,
948                    phase: mouse_binding::MousePhase::Wheel,
949                    affected_buttons: hashset! {},
950                    pressed_buttons: hashset! {},
951                    is_precision_scroll: None,
952                }),
953                device_descriptor: DEVICE_DESCRIPTOR.clone(),
954                event_time: zx::MonotonicInstant::from_nanos(0),
955                trace_id: None,
956            };
957            handler.clone().handle_unhandled_input_event(input_event).await;
958
959            // Send in the requested motion.
960            let input_event = input_device::UnhandledInputEvent {
961                device_event: input_device::InputDeviceEvent::Mouse(mouse_binding::MouseEvent {
962                    location: mouse_binding::MouseLocation::Relative(Default::default()),
963                    wheel_delta_v: match wheel_delta_v_mm {
964                        None => None,
965                        Some(delta) => Some(mouse_binding::WheelDelta {
966                            raw_data: mouse_binding::RawWheelDelta::Millimeters(delta),
967                            physical_pixel: None,
968                        }),
969                    },
970                    wheel_delta_h: match wheel_delta_h_mm {
971                        None => None,
972                        Some(delta) => Some(mouse_binding::WheelDelta {
973                            raw_data: mouse_binding::RawWheelDelta::Millimeters(delta),
974                            physical_pixel: None,
975                        }),
976                    },
977                    phase: mouse_binding::MousePhase::Wheel,
978                    affected_buttons: hashset! {},
979                    pressed_buttons: hashset! {},
980                    is_precision_scroll: None,
981                }),
982                device_descriptor: DEVICE_DESCRIPTOR.clone(),
983                event_time: zx::MonotonicInstant::from_nanos(duration.into_nanos()),
984                trace_id: None,
985            };
986            let transformed_events =
987                handler.clone().handle_unhandled_input_event(input_event).await;
988
989            assert_eq!(transformed_events.len(), 1);
990
991            if let input_device::InputEvent {
992                device_event:
993                    input_device::InputDeviceEvent::Mouse(mouse_binding::MouseEvent {
994                        wheel_delta_v: delta_v,
995                        wheel_delta_h: delta_h,
996                        ..
997                    }),
998                ..
999            } = transformed_events[0].clone()
1000            {
1001                return (delta_v, delta_h);
1002            } else {
1003                unreachable!()
1004            }
1005        }
1006
1007        fn velocity_to_mm(velocity_mm_per_sec: f32, duration: zx::MonotonicDuration) -> f32 {
1008            velocity_mm_per_sec * (duration.into_nanos() as f32 / 1E9)
1009        }
1010
1011        #[fuchsia::test(allow_stalls = false)]
1012        async fn low_speed_horizontal_scroll_scales_linearly() {
1013            const TICK_DURATION: zx::MonotonicDuration = zx::MonotonicDuration::from_millis(8);
1014            const MOTION_A_MM: f32 = 1.0 / COUNTS_PER_MM;
1015            const MOTION_B_MM: f32 = 2.0 / COUNTS_PER_MM;
1016            assert_lt!(
1017                MOTION_B_MM,
1018                velocity_to_mm(MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC, TICK_DURATION)
1019            );
1020
1021            let (_, scaled_a_h) =
1022                get_scaled_scroll_mm(None, Some(MOTION_A_MM), TICK_DURATION).await;
1023
1024            let (_, scaled_b_h) =
1025                get_scaled_scroll_mm(None, Some(MOTION_B_MM), TICK_DURATION).await;
1026
1027            match (scaled_a_h, scaled_b_h) {
1028                (Some(a_h), Some(b_h)) => {
1029                    assert_ne!(a_h.physical_pixel, None);
1030                    assert_ne!(b_h.physical_pixel, None);
1031                    assert_ne!(a_h.physical_pixel.unwrap(), 0.0);
1032                    assert_ne!(b_h.physical_pixel.unwrap(), 0.0);
1033                    assert_near!(
1034                        b_h.physical_pixel.unwrap() / a_h.physical_pixel.unwrap(),
1035                        2.0,
1036                        SCALE_EPSILON
1037                    );
1038                }
1039                _ => {
1040                    panic!("wheel delta is none");
1041                }
1042            }
1043        }
1044
1045        #[fuchsia::test(allow_stalls = false)]
1046        async fn low_speed_vertical_scroll_scales_linearly() {
1047            const TICK_DURATION: zx::MonotonicDuration = zx::MonotonicDuration::from_millis(8);
1048            const MOTION_A_MM: f32 = 1.0 / COUNTS_PER_MM;
1049            const MOTION_B_MM: f32 = 2.0 / COUNTS_PER_MM;
1050            assert_lt!(
1051                MOTION_B_MM,
1052                velocity_to_mm(MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC, TICK_DURATION)
1053            );
1054
1055            let (scaled_a_v, _) =
1056                get_scaled_scroll_mm(Some(MOTION_A_MM), None, TICK_DURATION).await;
1057
1058            let (scaled_b_v, _) =
1059                get_scaled_scroll_mm(Some(MOTION_B_MM), None, TICK_DURATION).await;
1060
1061            match (scaled_a_v, scaled_b_v) {
1062                (Some(a_v), Some(b_v)) => {
1063                    assert_ne!(a_v.physical_pixel, None);
1064                    assert_ne!(b_v.physical_pixel, None);
1065                    assert_near!(
1066                        b_v.physical_pixel.unwrap() / a_v.physical_pixel.unwrap(),
1067                        2.0,
1068                        SCALE_EPSILON
1069                    );
1070                }
1071                _ => {
1072                    panic!("wheel delta is none");
1073                }
1074            }
1075        }
1076
1077        #[fuchsia::test(allow_stalls = false)]
1078        async fn medium_speed_horizontal_scroll_scales_quadratically() {
1079            const TICK_DURATION: zx::MonotonicDuration = zx::MonotonicDuration::from_millis(8);
1080            const MOTION_A_MM: f32 = 7.0 / COUNTS_PER_MM;
1081            const MOTION_B_MM: f32 = 14.0 / COUNTS_PER_MM;
1082            assert_gt!(
1083                MOTION_A_MM,
1084                velocity_to_mm(MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC, TICK_DURATION)
1085            );
1086            assert_lt!(
1087                MOTION_B_MM,
1088                velocity_to_mm(MEDIUM_SPEED_RANGE_END_MM_PER_SEC, TICK_DURATION)
1089            );
1090
1091            let (_, scaled_a_h) =
1092                get_scaled_scroll_mm(None, Some(MOTION_A_MM), TICK_DURATION).await;
1093
1094            let (_, scaled_b_h) =
1095                get_scaled_scroll_mm(None, Some(MOTION_B_MM), TICK_DURATION).await;
1096
1097            match (scaled_a_h, scaled_b_h) {
1098                (Some(a_h), Some(b_h)) => {
1099                    assert_ne!(a_h.physical_pixel, None);
1100                    assert_ne!(b_h.physical_pixel, None);
1101                    assert_ne!(a_h.physical_pixel.unwrap(), 0.0);
1102                    assert_ne!(b_h.physical_pixel.unwrap(), 0.0);
1103                    assert_near!(
1104                        b_h.physical_pixel.unwrap() / a_h.physical_pixel.unwrap(),
1105                        4.0,
1106                        SCALE_EPSILON
1107                    );
1108                }
1109                _ => {
1110                    panic!("wheel delta is none");
1111                }
1112            }
1113        }
1114
1115        #[fuchsia::test(allow_stalls = false)]
1116        async fn medium_speed_vertical_scroll_scales_quadratically() {
1117            const TICK_DURATION: zx::MonotonicDuration = zx::MonotonicDuration::from_millis(8);
1118            const MOTION_A_MM: f32 = 7.0 / COUNTS_PER_MM;
1119            const MOTION_B_MM: f32 = 14.0 / COUNTS_PER_MM;
1120            assert_gt!(
1121                MOTION_A_MM,
1122                velocity_to_mm(MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC, TICK_DURATION)
1123            );
1124            assert_lt!(
1125                MOTION_B_MM,
1126                velocity_to_mm(MEDIUM_SPEED_RANGE_END_MM_PER_SEC, TICK_DURATION)
1127            );
1128
1129            let (scaled_a_v, _) =
1130                get_scaled_scroll_mm(Some(MOTION_A_MM), None, TICK_DURATION).await;
1131
1132            let (scaled_b_v, _) =
1133                get_scaled_scroll_mm(Some(MOTION_B_MM), None, TICK_DURATION).await;
1134
1135            match (scaled_a_v, scaled_b_v) {
1136                (Some(a_v), Some(b_v)) => {
1137                    assert_ne!(a_v.physical_pixel, None);
1138                    assert_ne!(b_v.physical_pixel, None);
1139                    assert_near!(
1140                        b_v.physical_pixel.unwrap() / a_v.physical_pixel.unwrap(),
1141                        4.0,
1142                        SCALE_EPSILON
1143                    );
1144                }
1145                _ => {
1146                    panic!("wheel delta is none");
1147                }
1148            }
1149        }
1150
1151        #[fuchsia::test(allow_stalls = false)]
1152        async fn high_speed_horizontal_scroll_scaling_is_inreasing() {
1153            const TICK_DURATION: zx::MonotonicDuration = zx::MonotonicDuration::from_millis(8);
1154            const MOTION_A_MM: f32 = 16.0 / COUNTS_PER_MM;
1155            const MOTION_B_MM: f32 = 20.0 / COUNTS_PER_MM;
1156            assert_gt!(
1157                MOTION_A_MM,
1158                velocity_to_mm(MEDIUM_SPEED_RANGE_END_MM_PER_SEC, TICK_DURATION)
1159            );
1160
1161            let (_, scaled_a_h) =
1162                get_scaled_scroll_mm(None, Some(MOTION_A_MM), TICK_DURATION).await;
1163
1164            let (_, scaled_b_h) =
1165                get_scaled_scroll_mm(None, Some(MOTION_B_MM), TICK_DURATION).await;
1166
1167            match (scaled_a_h, scaled_b_h) {
1168                (Some(a_h), Some(b_h)) => {
1169                    assert_ne!(a_h.physical_pixel, None);
1170                    assert_ne!(b_h.physical_pixel, None);
1171                    assert_ne!(a_h.physical_pixel.unwrap(), 0.0);
1172                    assert_ne!(b_h.physical_pixel.unwrap(), 0.0);
1173                    assert_gt!(b_h.physical_pixel.unwrap(), a_h.physical_pixel.unwrap());
1174                }
1175                _ => {
1176                    panic!("wheel delta is none");
1177                }
1178            }
1179        }
1180
1181        #[fuchsia::test(allow_stalls = false)]
1182        async fn high_speed_vertical_scroll_scaling_is_inreasing() {
1183            const TICK_DURATION: zx::MonotonicDuration = zx::MonotonicDuration::from_millis(8);
1184            const MOTION_A_MM: f32 = 16.0 / COUNTS_PER_MM;
1185            const MOTION_B_MM: f32 = 20.0 / COUNTS_PER_MM;
1186            assert_gt!(
1187                MOTION_A_MM,
1188                velocity_to_mm(MEDIUM_SPEED_RANGE_END_MM_PER_SEC, TICK_DURATION)
1189            );
1190
1191            let (scaled_a_v, _) =
1192                get_scaled_scroll_mm(Some(MOTION_A_MM), None, TICK_DURATION).await;
1193
1194            let (scaled_b_v, _) =
1195                get_scaled_scroll_mm(Some(MOTION_B_MM), None, TICK_DURATION).await;
1196
1197            match (scaled_a_v, scaled_b_v) {
1198                (Some(a_v), Some(b_v)) => {
1199                    assert_ne!(a_v.physical_pixel, None);
1200                    assert_ne!(b_v.physical_pixel, None);
1201                    assert_gt!(b_v.physical_pixel.unwrap(), a_v.physical_pixel.unwrap());
1202                }
1203                _ => {
1204                    panic!("wheel delta is none");
1205                }
1206            }
1207        }
1208    }
1209
1210    mod metadata_preservation {
1211        use super::*;
1212        use test_case::test_case;
1213
1214        #[test_case(mouse_binding::MouseEvent {
1215            location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
1216                millimeters: Position { x: 1.5 / COUNTS_PER_MM, y: 4.5 / COUNTS_PER_MM },
1217            }),
1218            wheel_delta_v: None,
1219            wheel_delta_h: None,
1220            phase: mouse_binding::MousePhase::Move,
1221            affected_buttons: hashset! {},
1222            pressed_buttons: hashset! {},
1223            is_precision_scroll: None,
1224        }; "move event")]
1225        #[test_case(mouse_binding::MouseEvent {
1226            location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
1227                millimeters: Position::zero(),
1228            }),
1229            wheel_delta_v: Some(mouse_binding::WheelDelta {
1230                raw_data: mouse_binding::RawWheelDelta::Ticks(1),
1231                physical_pixel: None,
1232            }),
1233            wheel_delta_h: None,
1234            phase: mouse_binding::MousePhase::Wheel,
1235            affected_buttons: hashset! {},
1236            pressed_buttons: hashset! {},
1237            is_precision_scroll: None,
1238        }; "wheel event")]
1239        #[fuchsia::test(allow_stalls = false)]
1240        async fn does_not_consume_event(event: mouse_binding::MouseEvent) {
1241            let inspector = fuchsia_inspect::Inspector::default();
1242            let test_node = inspector.root().create_child("test_node");
1243            let handler =
1244                PointerSensorScaleHandler::new(&test_node, metrics::MetricsLogger::default());
1245            let input_event = make_unhandled_input_event(event);
1246            assert_matches!(
1247                handler.clone().handle_unhandled_input_event(input_event).await.as_slice(),
1248                [input_device::InputEvent { handled: input_device::Handled::No, .. }]
1249            );
1250        }
1251
1252        // Downstream handlers, and components consuming the `MouseEvent`, may be
1253        // sensitive to the speed of motion. So it's important to preserve timestamps.
1254        #[test_case(mouse_binding::MouseEvent {
1255            location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
1256                millimeters: Position { x: 1.5 / COUNTS_PER_MM, y: 4.5 / COUNTS_PER_MM },
1257            }),
1258            wheel_delta_v: None,
1259            wheel_delta_h: None,
1260            phase: mouse_binding::MousePhase::Move,
1261            affected_buttons: hashset! {},
1262            pressed_buttons: hashset! {},
1263            is_precision_scroll: None,
1264        }; "move event")]
1265        #[test_case(mouse_binding::MouseEvent {
1266            location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
1267                millimeters: Position::zero(),
1268            }),
1269            wheel_delta_v: Some(mouse_binding::WheelDelta {
1270                raw_data: mouse_binding::RawWheelDelta::Ticks(1),
1271                physical_pixel: None,
1272            }),
1273            wheel_delta_h: None,
1274            phase: mouse_binding::MousePhase::Wheel,
1275            affected_buttons: hashset! {},
1276            pressed_buttons: hashset! {},
1277            is_precision_scroll: None,
1278        }; "wheel event")]
1279        #[fuchsia::test(allow_stalls = false)]
1280        async fn preserves_event_time(event: mouse_binding::MouseEvent) {
1281            let inspector = fuchsia_inspect::Inspector::default();
1282            let test_node = inspector.root().create_child("test_node");
1283            let handler =
1284                PointerSensorScaleHandler::new(&test_node, metrics::MetricsLogger::default());
1285            let mut input_event = make_unhandled_input_event(event);
1286            const EVENT_TIME: zx::MonotonicInstant = zx::MonotonicInstant::from_nanos(42);
1287            input_event.event_time = EVENT_TIME;
1288
1289            let events = handler.clone().handle_unhandled_input_event(input_event).await;
1290            assert_eq!(events.len(), 1, "{events:?} should be length 1");
1291            assert_eq!(events[0].event_time, EVENT_TIME);
1292        }
1293
1294        #[test_case(
1295            mouse_binding::MouseEvent {
1296                location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
1297                    millimeters: Position::zero(),
1298                }),
1299                wheel_delta_v: Some(mouse_binding::WheelDelta {
1300                    raw_data: mouse_binding::RawWheelDelta::Ticks(1),
1301                    physical_pixel: Some(1.0),
1302                }),
1303                wheel_delta_h: None,
1304                phase: mouse_binding::MousePhase::Wheel,
1305                affected_buttons: hashset! {},
1306                pressed_buttons: hashset! {},
1307                is_precision_scroll: Some(mouse_binding::PrecisionScroll::No),
1308            } => matches input_device::InputEvent {
1309                device_event: input_device::InputDeviceEvent::Mouse(mouse_binding::MouseEvent {
1310                    is_precision_scroll: Some(mouse_binding::PrecisionScroll::No),
1311                    ..
1312                }),
1313                ..
1314            }; "no")]
1315        #[test_case(
1316            mouse_binding::MouseEvent {
1317                location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
1318                    millimeters: Position::zero(),
1319                }),
1320                wheel_delta_v: Some(mouse_binding::WheelDelta {
1321                    raw_data: mouse_binding::RawWheelDelta::Ticks(1),
1322                    physical_pixel: Some(1.0),
1323                }),
1324                wheel_delta_h: None,
1325                phase: mouse_binding::MousePhase::Wheel,
1326                affected_buttons: hashset! {},
1327                pressed_buttons: hashset! {},
1328                is_precision_scroll: Some(mouse_binding::PrecisionScroll::Yes),
1329            } => matches input_device::InputEvent {
1330                device_event: input_device::InputDeviceEvent::Mouse(mouse_binding::MouseEvent {
1331                    is_precision_scroll: Some(mouse_binding::PrecisionScroll::Yes),
1332                    ..
1333                }),
1334                ..
1335            }; "yes")]
1336        #[fuchsia::test(allow_stalls = false)]
1337        async fn preserves_is_precision_scroll(
1338            event: mouse_binding::MouseEvent,
1339        ) -> input_device::InputEvent {
1340            let inspector = fuchsia_inspect::Inspector::default();
1341            let test_node = inspector.root().create_child("test_node");
1342            let handler =
1343                PointerSensorScaleHandler::new(&test_node, metrics::MetricsLogger::default());
1344            let input_event = make_unhandled_input_event(event);
1345
1346            handler.clone().handle_unhandled_input_event(input_event).await[0].clone()
1347        }
1348    }
1349}