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