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