Skip to main content

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