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