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