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