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