| // Copyright 2022 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| //! |
| use { |
| crate::{input_device, input_handler::UnhandledInputHandler, mouse_binding, utils::Position}, |
| async_trait::async_trait, |
| fuchsia_syslog::fx_log_err, |
| fuchsia_zircon as zx, |
| std::{cell::RefCell, convert::From, num::FpCategory, option::Option, rc::Rc}, |
| }; |
| |
| pub struct PointerMotionSensorScaleHandler { |
| mutable_state: RefCell<MutableState>, |
| } |
| |
| struct MutableState { |
| /// The time of the last processed mouse move event. |
| last_move_timestamp: Option<zx::Time>, |
| } |
| |
| #[async_trait(?Send)] |
| impl UnhandledInputHandler for PointerMotionSensorScaleHandler { |
| async fn handle_unhandled_input_event( |
| self: Rc<Self>, |
| unhandled_input_event: input_device::UnhandledInputEvent, |
| ) -> Vec<input_device::InputEvent> { |
| match unhandled_input_event { |
| // TODO(https://fxbug.dev/98699) Disable scaling when in immersive mode. |
| input_device::UnhandledInputEvent { |
| device_event: |
| input_device::InputDeviceEvent::Mouse(mouse_binding::MouseEvent { |
| location: |
| mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation { |
| counts: raw_motion, |
| millimeters: _, |
| }), |
| wheel_delta_v, |
| wheel_delta_h, |
| // Only the `Move` phase carries non-zero motion. |
| phase: phase @ mouse_binding::MousePhase::Move, |
| affected_buttons, |
| pressed_buttons, |
| }), |
| device_descriptor: device_descriptor @ input_device::InputDeviceDescriptor::Mouse(_), |
| event_time, |
| trace_id: _, |
| } => { |
| let scaled_motion = self.scale_motion(raw_motion, event_time); |
| let input_event = input_device::InputEvent { |
| device_event: input_device::InputDeviceEvent::Mouse( |
| mouse_binding::MouseEvent { |
| location: mouse_binding::MouseLocation::Relative( |
| mouse_binding::RelativeLocation { |
| counts: scaled_motion, |
| // TODO(https://fxbug.dev/102568): Implement millimeters. |
| millimeters: Position::zero(), |
| }, |
| ), |
| wheel_delta_v, |
| wheel_delta_h, |
| phase, |
| affected_buttons, |
| pressed_buttons, |
| }, |
| ), |
| device_descriptor, |
| event_time, |
| handled: input_device::Handled::No, |
| trace_id: None, |
| }; |
| vec![input_event] |
| } |
| _ => vec![input_device::InputEvent::from(unhandled_input_event)], |
| } |
| } |
| } |
| |
| // The absolute value of the movement reported by the mouse when the mouse |
| // moves one millimeter. |
| // * This value was measured on an Atlas touchpad operating in mouse mode, |
| // and is ~300 counts-per-inch. |
| // * This value _should_ be read from the mouse's input descriptor, once |
| // the value is added to said descriptor. |
| // |
| // TODO(https://fxbug.dev/98919): Use device-specific values. |
| const COUNTS_PER_MM: f32 = 12.0; |
| |
| // The minimum reasonable delay between intentional mouse movements. |
| // This value |
| // * Is used to compensate for time compression if the driver gets |
| // backlogged. |
| // * Is set to accommodate up to 10 kHZ event reporting. |
| // |
| // TODO(https://fxbug.dev/98920): Use the polling rate instead of event timestamps. |
| const MIN_PLAUSIBLE_EVENT_DELAY: zx::Duration = zx::Duration::from_micros(100); |
| |
| // The maximum reasonable delay between intentional mouse movements. |
| // This value is used to compute speed for the first mouse motion after |
| // a long idle period. |
| // |
| // Alternatively: |
| // 1. The code could use the uncapped delay. However, this would lead to |
| // very slow initial motion after a long idle period. |
| // 2. Wait until a second report comes in. However, older mice generate |
| // reports at 125 HZ, which would mean an 8 msec delay. |
| // |
| // TODO(https://fxbug.dev/98920): Use the polling rate instead of event timestamps. |
| const MAX_PLAUSIBLE_EVENT_DELAY: zx::Duration = zx::Duration::from_millis(50); |
| |
| const MAX_SENSOR_COUNTS_PER_INCH: f32 = 20_000.0; // From https://sensor.fyi/sensors |
| const MAX_SENSOR_COUNTS_PER_MM: f32 = MAX_SENSOR_COUNTS_PER_INCH / 12.7; |
| const MIN_MEASURABLE_DISTANCE_MM: f32 = 1.0 / MAX_SENSOR_COUNTS_PER_MM; |
| const MAX_PLAUSIBLE_EVENT_DELAY_SECS: f32 = MAX_PLAUSIBLE_EVENT_DELAY.into_nanos() as f32 / 1E9; |
| const MIN_MEASURABLE_VELOCITY_MM_PER_SEC: f32 = |
| MIN_MEASURABLE_DISTANCE_MM / MAX_PLAUSIBLE_EVENT_DELAY_SECS; |
| |
| // Define the buckets which determine which mapping to use. |
| // * Speeds below the beginning of the medium range use the low-speed mapping. |
| // * Speeds within the medium range use the medium-speed mapping. |
| // * Speeds above the end of the medium range use the high-speed mapping. |
| const MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC: f32 = 32.0; |
| const MEDIUM_SPEED_RANGE_END_MM_PER_SEC: f32 = 150.0; |
| |
| // A linear factor affecting the responsiveness of the pointer to motion. |
| // A higher numbness indicates lower responsiveness. |
| const NUMBNESS: f32 = 37.5; |
| |
| impl PointerMotionSensorScaleHandler { |
| /// Creates a new [`PointerMotionSensorScaleHandler`]. |
| /// |
| /// Returns `Rc<Self>`. |
| pub fn new() -> Rc<Self> { |
| Rc::new(Self { mutable_state: RefCell::new(MutableState { last_move_timestamp: None }) }) |
| } |
| |
| // Linearly scales `movement_mm_per_sec`. |
| // |
| // Given the values of `MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC` and |
| // `NUMBNESS` above, this results in downscaling the motion. |
| fn scale_low_speed_motion(movement_mm_per_sec: f32) -> f32 { |
| const LINEAR_SCALE_FACTOR: f32 = MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC / NUMBNESS; |
| LINEAR_SCALE_FACTOR * movement_mm_per_sec |
| } |
| |
| // Quadratically scales `movement_mm_per_sec`. |
| // |
| // The scale factor is chosen so that the composite curve is |
| // continuous as the speed transitions from the low-speed |
| // bucket to the medium-speed bucket. |
| // |
| // Note that the composite curve is _not_ differentiable at the |
| // transition from low-speed to medium-speed, since the |
| // slope on the left side of the point |
| // (MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC / NUMBNESS) |
| // is different from the slope on the right side of the point |
| // (2 * MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC / NUMBNESS). |
| // |
| // However, the transition works well enough in practice. |
| fn scale_medium_speed_motion(movement_mm_per_sec: f32) -> f32 { |
| const QUARDRATIC_SCALE_FACTOR: f32 = 1.0 / NUMBNESS; |
| QUARDRATIC_SCALE_FACTOR * movement_mm_per_sec * movement_mm_per_sec |
| } |
| |
| // Linearly scales `movement_mm_per_sec`. |
| // |
| // The parameters are chosen so that |
| // 1. The composite curve is continuous as the speed transitions |
| // from the medium-speed bucket to the high-speed bucket. |
| // 2. The composite curve is differentiable. |
| fn scale_high_speed_motion(movement_mm_per_sec: f32) -> f32 { |
| // Use linear scaling equal to the slope of `scale_medium_speed_motion()` |
| // at the transition point. |
| const LINEAR_SCALE_FACTOR: f32 = 2.0 * (MEDIUM_SPEED_RANGE_END_MM_PER_SEC / NUMBNESS); |
| |
| // Compute offset so the composite curve is continuous. |
| const Y_AT_MEDIUM_SPEED_RANGE_END_MM_PER_SEC: f32 = |
| MEDIUM_SPEED_RANGE_END_MM_PER_SEC * MEDIUM_SPEED_RANGE_END_MM_PER_SEC / NUMBNESS; |
| const OFFSET: f32 = Y_AT_MEDIUM_SPEED_RANGE_END_MM_PER_SEC |
| - LINEAR_SCALE_FACTOR * MEDIUM_SPEED_RANGE_END_MM_PER_SEC; |
| |
| // Apply the computed transformation. |
| LINEAR_SCALE_FACTOR * movement_mm_per_sec + OFFSET |
| } |
| |
| // Scales Euclidean velocity by one of the scale_*_speed_motion() functions above, |
| // choosing the function based on `MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC` and |
| // `MEDIUM_SPEED_RANGE_END_MM_PER_SEC`. |
| fn scale_euclidean_velocity(raw_velocity: f32) -> f32 { |
| if (0.0..MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC).contains(&raw_velocity) { |
| Self::scale_low_speed_motion(raw_velocity) |
| } else if (MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC..MEDIUM_SPEED_RANGE_END_MM_PER_SEC) |
| .contains(&raw_velocity) |
| { |
| Self::scale_medium_speed_motion(raw_velocity) |
| } else { |
| Self::scale_high_speed_motion(raw_velocity) |
| } |
| } |
| |
| /// Scales `movement_counts`. |
| fn scale_motion(&self, movement_counts: Position, event_time: zx::Time) -> Position { |
| // Determine the duration of this `movement`. |
| let elapsed_time_secs = |
| match self.mutable_state.borrow_mut().last_move_timestamp.replace(event_time) { |
| Some(last_event_time) => (event_time - last_event_time) |
| .clamp(MIN_PLAUSIBLE_EVENT_DELAY, MAX_PLAUSIBLE_EVENT_DELAY), |
| None => MAX_PLAUSIBLE_EVENT_DELAY, |
| } |
| .into_nanos() as f32 |
| / 1E9; |
| |
| // Compute the velocity in each dimension. |
| let x_mm_per_sec = movement_counts.x / COUNTS_PER_MM / elapsed_time_secs; |
| let y_mm_per_sec = movement_counts.y / COUNTS_PER_MM / elapsed_time_secs; |
| |
| let euclidean_velocity = |
| f32::sqrt(x_mm_per_sec * x_mm_per_sec + y_mm_per_sec * y_mm_per_sec); |
| if euclidean_velocity < MIN_MEASURABLE_VELOCITY_MM_PER_SEC { |
| // Avoid division by zero that would come from computing `scale_factor` below. |
| return movement_counts; |
| } |
| |
| // Compute the scaling factor to be applied to each dimension. |
| // |
| // Geometrically, this is a bit dodgy when there's movement along both |
| // dimensions. Specifically: the `OFFSET` for high-speed motion should be |
| // constant, but the way its used here scales the offset based on velocity. |
| // |
| // Nonetheless, this works well enough in practice. |
| let scale_factor = Self::scale_euclidean_velocity(euclidean_velocity) / euclidean_velocity; |
| |
| // Apply the scale factor and return the result. |
| let scaled_movement_counts = scale_factor * movement_counts; |
| |
| match (scaled_movement_counts.x.classify(), scaled_movement_counts.y.classify()) { |
| (FpCategory::Infinite | FpCategory::Nan, _) |
| | (_, FpCategory::Infinite | FpCategory::Nan) => { |
| // Backstop, in case the code above missed some cases of bad arithmetic. |
| // Avoid sending `Infinite` or `Nan` values, since such values will |
| // poison the `current_position` in `MouseInjectorHandlerInner`. |
| // That manifests as the pointer becoming invisible, and never |
| // moving again. |
| // |
| // TODO(https://fxbug.dev/98995) Add a triage rule to highlight the |
| // implications of this message. |
| fx_log_err!( |
| "skipped motion; scaled movement of {:?} is infinite or NaN; x is {:?}, and y is {:?}", |
| scaled_movement_counts, |
| scaled_movement_counts.x.classify(), |
| scaled_movement_counts.y.classify(), |
| ); |
| Position { x: 0.0, y: 0.0 } |
| } |
| _ => scaled_movement_counts, |
| } |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use { |
| super::*, |
| assert_matches::assert_matches, |
| fuchsia_zircon as zx, |
| maplit::hashset, |
| std::cell::Cell, |
| test_util::{assert_gt, assert_lt, assert_near}, |
| }; |
| |
| const DEVICE_DESCRIPTOR: input_device::InputDeviceDescriptor = |
| input_device::InputDeviceDescriptor::Mouse(mouse_binding::MouseDeviceDescriptor { |
| device_id: 0, |
| absolute_x_range: None, |
| absolute_y_range: None, |
| wheel_v_range: None, |
| wheel_h_range: None, |
| buttons: None, |
| }); |
| |
| // Maximum tolerable difference between "equal" scale factors. This is |
| // likely higher than FP rounding error can explain, but still small |
| // enough that there would be no user-perceptible difference. |
| // |
| // Rationale for not being user-perceptible: this requires the raw |
| // movement to have a count of 100,000, before there's a unit change |
| // in the scaled motion. |
| // |
| // On even the highest resolution sensor (per https://sensor.fyi/sensors), |
| // that would require 127mm (5 inches) of motion within one sampling |
| // interval. |
| // |
| // In the unlikely case that the high resolution sensor is paired |
| // with a low polling rate, that works out to 127mm/8msec, or _at least_ |
| // 57 km/hr. |
| const SCALE_EPSILON: f32 = 1.0 / 100_000.0; |
| |
| std::thread_local! {static NEXT_EVENT_TIME: Cell<i64> = Cell::new(0)} |
| |
| fn make_unhandled_input_event( |
| mouse_event: mouse_binding::MouseEvent, |
| ) -> input_device::UnhandledInputEvent { |
| let event_time = NEXT_EVENT_TIME.with(|t| { |
| let old = t.get(); |
| t.set(old + 1); |
| old |
| }); |
| input_device::UnhandledInputEvent { |
| device_event: input_device::InputDeviceEvent::Mouse(mouse_event), |
| device_descriptor: DEVICE_DESCRIPTOR.clone(), |
| event_time: zx::Time::from_nanos(event_time), |
| trace_id: None, |
| } |
| } |
| |
| // While its generally preferred to write tests against the public API of |
| // a module, these tests |
| // 1. Can't be written against the public API (since that API doesn't |
| // provide a way to control which curve is used for scaling), and |
| // 2. Validate important properties of the module. |
| mod internal_computations { |
| use super::*; |
| |
| #[fuchsia::test] |
| fn transition_from_low_to_medium_is_continuous() { |
| assert_near!( |
| PointerMotionSensorScaleHandler::scale_low_speed_motion( |
| MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC |
| ), |
| PointerMotionSensorScaleHandler::scale_medium_speed_motion( |
| MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC |
| ), |
| SCALE_EPSILON |
| ); |
| } |
| |
| // As noted in `scale_motion()`, the offset will be applied imperfectly, |
| // so the externally visible transition may not be continuous. |
| // |
| // However, it's still valuable to verify that the internal building block |
| // works as intended. |
| #[fuchsia::test] |
| fn transition_from_medium_to_high_is_continuous() { |
| assert_near!( |
| PointerMotionSensorScaleHandler::scale_medium_speed_motion( |
| MEDIUM_SPEED_RANGE_END_MM_PER_SEC |
| ), |
| PointerMotionSensorScaleHandler::scale_high_speed_motion( |
| MEDIUM_SPEED_RANGE_END_MM_PER_SEC |
| ), |
| SCALE_EPSILON |
| ); |
| } |
| } |
| |
| mod motion_scaling { |
| use super::*; |
| |
| #[ignore] |
| #[fuchsia::test(allow_stalls = false)] |
| async fn plot_example_curve() { |
| let duration = zx::Duration::from_millis(8); |
| for count in 1..1000 { |
| let scaled_count = |
| get_scaled_motion(Position { x: count as f32, y: 0.0 }, duration).await; |
| fx_log_err!("{}, {}", count, scaled_count.x); |
| } |
| } |
| |
| async fn get_scaled_motion(movement_counts: Position, duration: zx::Duration) -> Position { |
| let handler = PointerMotionSensorScaleHandler::new(); |
| |
| // Send a don't-care value through to seed the last timestamp. |
| let input_event = input_device::UnhandledInputEvent { |
| device_event: input_device::InputDeviceEvent::Mouse(mouse_binding::MouseEvent { |
| location: mouse_binding::MouseLocation::Relative(Default::default()), |
| wheel_delta_v: None, |
| wheel_delta_h: None, |
| phase: mouse_binding::MousePhase::Move, |
| affected_buttons: hashset! {}, |
| pressed_buttons: hashset! {}, |
| }), |
| device_descriptor: DEVICE_DESCRIPTOR.clone(), |
| event_time: zx::Time::from_nanos(0), |
| trace_id: None, |
| }; |
| handler.clone().handle_unhandled_input_event(input_event).await; |
| |
| // Send in the requested motion. |
| let input_event = input_device::UnhandledInputEvent { |
| device_event: input_device::InputDeviceEvent::Mouse(mouse_binding::MouseEvent { |
| location: mouse_binding::MouseLocation::Relative( |
| mouse_binding::RelativeLocation { |
| counts: movement_counts, |
| millimeters: Position::zero(), |
| }, |
| ), |
| wheel_delta_v: None, |
| wheel_delta_h: None, |
| phase: mouse_binding::MousePhase::Move, |
| affected_buttons: hashset! {}, |
| pressed_buttons: hashset! {}, |
| }), |
| device_descriptor: DEVICE_DESCRIPTOR.clone(), |
| event_time: zx::Time::from_nanos(duration.into_nanos()), |
| trace_id: None, |
| }; |
| let transformed_events = |
| handler.clone().handle_unhandled_input_event(input_event).await; |
| |
| // Provide a useful debug message if the transformed event doesn't have the expected |
| // overall structure. |
| assert_matches!( |
| transformed_events.as_slice(), |
| [input_device::InputEvent { |
| device_event: input_device::InputDeviceEvent::Mouse( |
| mouse_binding::MouseEvent { |
| location: mouse_binding::MouseLocation::Relative( |
| mouse_binding::RelativeLocation { .. } |
| ), |
| .. |
| } |
| ), |
| .. |
| }] |
| ); |
| |
| // Return the transformed motion. |
| if let input_device::InputEvent { |
| device_event: |
| input_device::InputDeviceEvent::Mouse(mouse_binding::MouseEvent { |
| location: |
| mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation { |
| counts: movement_counts, |
| millimeters: _, |
| }), |
| .. |
| }), |
| .. |
| } = transformed_events[0] |
| { |
| movement_counts |
| } else { |
| unreachable!() |
| } |
| } |
| |
| fn velocity_to_count(velocity_mm_per_sec: f32, duration: zx::Duration) -> f32 { |
| velocity_mm_per_sec * (duration.into_nanos() as f32 / 1E9) * COUNTS_PER_MM |
| } |
| |
| #[fuchsia::test(allow_stalls = false)] |
| async fn low_speed_horizontal_motion_scales_linearly() { |
| const TICK_DURATION: zx::Duration = zx::Duration::from_millis(8); |
| const MOTION_A_COUNTS: f32 = 1.0; |
| const MOTION_B_COUNTS: f32 = 2.0; |
| assert_lt!( |
| MOTION_B_COUNTS, |
| velocity_to_count(MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC, TICK_DURATION) |
| ); |
| |
| let scaled_a = |
| get_scaled_motion(Position { x: MOTION_A_COUNTS, y: 0.0 }, TICK_DURATION).await; |
| let scaled_b = |
| get_scaled_motion(Position { x: MOTION_B_COUNTS, y: 0.0 }, TICK_DURATION).await; |
| assert_near!(scaled_b.x / scaled_a.x, 2.0, SCALE_EPSILON); |
| } |
| |
| #[fuchsia::test(allow_stalls = false)] |
| async fn low_speed_vertical_motion_scales_linearly() { |
| const TICK_DURATION: zx::Duration = zx::Duration::from_millis(8); |
| const MOTION_A_COUNTS: f32 = 1.0; |
| const MOTION_B_COUNTS: f32 = 2.0; |
| assert_lt!( |
| MOTION_B_COUNTS, |
| velocity_to_count(MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC, TICK_DURATION) |
| ); |
| |
| let scaled_a = |
| get_scaled_motion(Position { x: 0.0, y: MOTION_A_COUNTS }, TICK_DURATION).await; |
| let scaled_b = |
| get_scaled_motion(Position { x: 0.0, y: MOTION_B_COUNTS }, TICK_DURATION).await; |
| assert_near!(scaled_b.y / scaled_a.y, 2.0, SCALE_EPSILON); |
| } |
| |
| #[fuchsia::test(allow_stalls = false)] |
| async fn low_speed_45degree_motion_scales_dimensions_equally() { |
| const TICK_DURATION: zx::Duration = zx::Duration::from_millis(8); |
| const MOTION_COUNTS: f32 = 1.0; |
| assert_lt!( |
| MOTION_COUNTS, |
| velocity_to_count(MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC, TICK_DURATION) |
| ); |
| |
| let scaled = |
| get_scaled_motion(Position { x: MOTION_COUNTS, y: MOTION_COUNTS }, TICK_DURATION) |
| .await; |
| assert_near!(scaled.x, scaled.y, SCALE_EPSILON); |
| } |
| |
| #[fuchsia::test(allow_stalls = false)] |
| async fn medium_speed_motion_scales_quadratically() { |
| const TICK_DURATION: zx::Duration = zx::Duration::from_millis(8); |
| const MOTION_A_COUNTS: f32 = 7.0; |
| const MOTION_B_COUNTS: f32 = 14.0; |
| assert_gt!( |
| MOTION_A_COUNTS, |
| velocity_to_count(MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC, TICK_DURATION) |
| ); |
| assert_lt!( |
| MOTION_B_COUNTS, |
| velocity_to_count(MEDIUM_SPEED_RANGE_END_MM_PER_SEC, TICK_DURATION) |
| ); |
| |
| let scaled_a = |
| get_scaled_motion(Position { x: MOTION_A_COUNTS, y: 0.0 }, TICK_DURATION).await; |
| let scaled_b = |
| get_scaled_motion(Position { x: MOTION_B_COUNTS, y: 0.0 }, TICK_DURATION).await; |
| assert_near!(scaled_b.x / scaled_a.x, 4.0, SCALE_EPSILON); |
| } |
| |
| // Given the handling of `OFFSET` for high-speed motion, (see comment |
| // in `scale_motion()`), high speed motion scaling is _not_ linear for |
| // the range of values of practical interest. |
| // |
| // Thus, this tests verifies a weaker property. |
| #[fuchsia::test(allow_stalls = false)] |
| async fn high_speed_motion_scaling_is_increasing() { |
| const TICK_DURATION: zx::Duration = zx::Duration::from_millis(8); |
| const MOTION_A_COUNTS: f32 = 16.0; |
| const MOTION_B_COUNTS: f32 = 20.0; |
| assert_gt!( |
| MOTION_A_COUNTS, |
| velocity_to_count(MEDIUM_SPEED_RANGE_END_MM_PER_SEC, TICK_DURATION) |
| ); |
| |
| let scaled_a = |
| get_scaled_motion(Position { x: MOTION_A_COUNTS, y: 0.0 }, TICK_DURATION).await; |
| let scaled_b = |
| get_scaled_motion(Position { x: MOTION_B_COUNTS, y: 0.0 }, TICK_DURATION).await; |
| assert_gt!(scaled_b.x, scaled_a.x) |
| } |
| |
| #[fuchsia::test(allow_stalls = false)] |
| async fn zero_motion_maps_to_zero_motion() { |
| const TICK_DURATION: zx::Duration = zx::Duration::from_millis(8); |
| let scaled = get_scaled_motion(Position { x: 0.0, y: 0.0 }, TICK_DURATION).await; |
| assert_eq!(scaled, Position::zero()) |
| } |
| |
| #[fuchsia::test(allow_stalls = false)] |
| async fn zero_duration_does_not_crash() { |
| get_scaled_motion(Position { x: 1.0, y: 0.0 }, zx::Duration::from_millis(0)).await; |
| } |
| } |
| |
| mod metadata_preservation { |
| use super::*; |
| |
| #[fuchsia::test(allow_stalls = false)] |
| async fn does_not_consume_event() { |
| let handler = PointerMotionSensorScaleHandler::new(); |
| let input_event = make_unhandled_input_event(mouse_binding::MouseEvent { |
| location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation { |
| counts: Position { x: 1.5, y: 4.5 }, |
| millimeters: Position::zero(), |
| }), |
| wheel_delta_v: None, |
| wheel_delta_h: None, |
| phase: mouse_binding::MousePhase::Move, |
| affected_buttons: hashset! {}, |
| pressed_buttons: hashset! {}, |
| }); |
| assert_matches!( |
| handler.clone().handle_unhandled_input_event(input_event).await.as_slice(), |
| [input_device::InputEvent { handled: input_device::Handled::No, .. }] |
| ); |
| } |
| |
| // Downstream handlers, and components consuming the `MouseEvent`, may be |
| // sensitive to the speed of motion. So it's important to preserve timestamps. |
| #[fuchsia::test(allow_stalls = false)] |
| async fn preserves_event_time() { |
| let handler = PointerMotionSensorScaleHandler::new(); |
| let mut input_event = make_unhandled_input_event(mouse_binding::MouseEvent { |
| location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation { |
| counts: Position { x: 1.5, y: 4.5 }, |
| millimeters: Position::zero(), |
| }), |
| wheel_delta_v: None, |
| wheel_delta_h: None, |
| phase: mouse_binding::MousePhase::Move, |
| affected_buttons: hashset! {}, |
| pressed_buttons: hashset! {}, |
| }); |
| const EVENT_TIME: zx::Time = zx::Time::from_nanos(42); |
| input_event.event_time = EVENT_TIME; |
| assert_matches!( |
| handler.clone().handle_unhandled_input_event(input_event).await.as_slice(), |
| [input_device::InputEvent { event_time: EVENT_TIME, .. }] |
| ); |
| } |
| } |
| } |