blob: 5fc3f630543f1904985a54dacee3f1d185bd580d [file] [log] [blame]
// 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::{InputHandlerStatus, UnhandledInputHandler},
metrics, mouse_binding,
utils::Position,
},
async_trait::async_trait,
fuchsia_inspect::health::Reporter,
fuchsia_zircon as zx,
metrics_registry::*,
std::{cell::RefCell, num::FpCategory, rc::Rc},
};
pub struct PointerSensorScaleHandler {
mutable_state: RefCell<MutableState>,
/// The inventory of this handler's Inspect status.
pub inspect_status: InputHandlerStatus,
/// The metrics logger.
metrics_logger: metrics::MetricsLogger,
}
struct MutableState {
/// The time of the last processed mouse move event.
last_move_timestamp: Option<zx::Time>,
/// The time of the last processed mouse scroll event.
last_scroll_timestamp: Option<zx::Time>,
}
/// For tick based scrolling, PointerSensorScaleHandler scales tick * 120 to logical
/// pixel.
const PIXELS_PER_TICK: f32 = 120.0;
/// TODO(https://fxbug.dev/42059911): Temporary apply a linear scale factor to scroll to make it feel
/// faster.
const SCALE_SCROLL: f32 = 2.0;
#[async_trait(?Send)]
impl UnhandledInputHandler for PointerSensorScaleHandler {
async fn handle_unhandled_input_event(
self: Rc<Self>,
unhandled_input_event: input_device::UnhandledInputEvent,
) -> Vec<input_device::InputEvent> {
match unhandled_input_event.clone() {
input_device::UnhandledInputEvent {
device_event:
input_device::InputDeviceEvent::Mouse(mouse_binding::MouseEvent {
location:
mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
millimeters: raw_motion,
}),
wheel_delta_v,
wheel_delta_h,
// Only the `Move` phase carries non-zero motion.
phase: phase @ mouse_binding::MousePhase::Move,
affected_buttons,
pressed_buttons,
is_precision_scroll,
}),
device_descriptor:
input_device::InputDeviceDescriptor::Mouse(mouse_binding::MouseDeviceDescriptor {
absolute_x_range,
absolute_y_range,
buttons,
counts_per_mm,
device_id,
wheel_h_range,
wheel_v_range,
}),
event_time,
trace_id: _,
} => {
self.inspect_status
.count_received_event(input_device::InputEvent::from(unhandled_input_event));
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 { millimeters: scaled_motion },
),
wheel_delta_v,
wheel_delta_h,
phase,
affected_buttons,
pressed_buttons,
is_precision_scroll,
},
),
device_descriptor: input_device::InputDeviceDescriptor::Mouse(
mouse_binding::MouseDeviceDescriptor {
absolute_x_range,
absolute_y_range,
buttons,
counts_per_mm,
device_id,
wheel_h_range,
wheel_v_range,
},
),
event_time,
handled: input_device::Handled::No,
trace_id: None,
};
vec![input_event]
}
input_device::UnhandledInputEvent {
device_event:
input_device::InputDeviceEvent::Mouse(mouse_binding::MouseEvent {
location,
wheel_delta_v,
wheel_delta_h,
phase: phase @ mouse_binding::MousePhase::Wheel,
affected_buttons,
pressed_buttons,
is_precision_scroll,
}),
device_descriptor:
input_device::InputDeviceDescriptor::Mouse(mouse_binding::MouseDeviceDescriptor {
absolute_x_range,
absolute_y_range,
buttons,
counts_per_mm,
device_id,
wheel_h_range,
wheel_v_range,
}),
event_time,
trace_id: _,
} => {
self.inspect_status
.count_received_event(input_device::InputEvent::from(unhandled_input_event));
let scaled_wheel_delta_v = self.scale_scroll(wheel_delta_v, event_time);
let scaled_wheel_delta_h = self.scale_scroll(wheel_delta_h, event_time);
let input_event = input_device::InputEvent {
device_event: input_device::InputDeviceEvent::Mouse(
mouse_binding::MouseEvent {
location,
wheel_delta_v: scaled_wheel_delta_v,
wheel_delta_h: scaled_wheel_delta_h,
phase,
affected_buttons,
pressed_buttons,
is_precision_scroll,
},
),
device_descriptor: input_device::InputDeviceDescriptor::Mouse(
mouse_binding::MouseDeviceDescriptor {
absolute_x_range,
absolute_y_range,
buttons,
counts_per_mm,
device_id,
wheel_h_range,
wheel_v_range,
},
),
event_time,
handled: input_device::Handled::No,
trace_id: None,
};
vec![input_event]
}
_ => vec![input_device::InputEvent::from(unhandled_input_event)],
}
}
fn set_handler_healthy(self: std::rc::Rc<Self>) {
self.inspect_status.health_node.borrow_mut().set_ok();
}
fn set_handler_unhealthy(self: std::rc::Rc<Self>, msg: &str) {
self.inspect_status.health_node.borrow_mut().set_unhealthy(msg);
}
}
// 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/42181307): 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/42181307): 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 PointerSensorScaleHandler {
/// Creates a new [`PointerSensorScaleHandler`].
///
/// Returns `Rc<Self>`.
pub fn new(
input_handlers_node: &fuchsia_inspect::Node,
metrics_logger: metrics::MetricsLogger,
) -> Rc<Self> {
let inspect_status = InputHandlerStatus::new(
input_handlers_node,
"pointer_sensor_scale_handler",
/* generates_events */ false,
);
Rc::new(Self {
mutable_state: RefCell::new(MutableState {
last_move_timestamp: None,
last_scroll_timestamp: None,
}),
inspect_status,
metrics_logger,
})
}
// 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(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(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(movement_mm_per_sec: f32) -> f32 {
// Use linear scaling equal to the slope of `scale_medium_speed()`
// 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(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(raw_velocity)
} else {
Self::scale_high_speed(raw_velocity)
}
}
/// Scales `movement_mm`.
fn scale_motion(&self, movement_mm: 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_mm.x / elapsed_time_secs;
let y_mm_per_sec = movement_mm.y / 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_mm;
}
// 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_mm = scale_factor * movement_mm;
match (scaled_movement_mm.x.classify(), scaled_movement_mm.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/42181389) Add a triage rule to highlight the
// implications of this message.
self.metrics_logger.log_error(
InputPipelineErrorMetricDimensionEvent::PointerSensorScaleHandlerScaledMotionInvalid,
std::format!(
"skipped motion; scaled movement of {:?} is infinite or NaN; x is {:?}, and y is {:?}",
scaled_movement_mm,
scaled_movement_mm.x.classify(),
scaled_movement_mm.y.classify(),
));
Position { x: 0.0, y: 0.0 }
}
_ => scaled_movement_mm,
}
}
/// `scroll_mm` scale with the curve algorithm.
/// `scroll_tick` scale with 120.
fn scale_scroll(
&self,
wheel_delta: Option<mouse_binding::WheelDelta>,
event_time: zx::Time,
) -> Option<mouse_binding::WheelDelta> {
match wheel_delta {
None => None,
Some(mouse_binding::WheelDelta {
raw_data: mouse_binding::RawWheelDelta::Ticks(tick),
..
}) => Some(mouse_binding::WheelDelta {
raw_data: mouse_binding::RawWheelDelta::Ticks(tick),
physical_pixel: Some(tick as f32 * PIXELS_PER_TICK),
}),
Some(mouse_binding::WheelDelta {
raw_data: mouse_binding::RawWheelDelta::Millimeters(mm),
..
}) => {
// Determine the duration of this `scroll`.
let elapsed_time_secs =
match self.mutable_state.borrow_mut().last_scroll_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;
let velocity = mm.abs() / elapsed_time_secs;
if velocity < MIN_MEASURABLE_VELOCITY_MM_PER_SEC {
// Avoid division by zero that would come from computing
// `scale_factor` below.
return Some(mouse_binding::WheelDelta {
raw_data: mouse_binding::RawWheelDelta::Millimeters(mm),
physical_pixel: Some(SCALE_SCROLL * mm),
});
}
let scale_factor = Self::scale_euclidean_velocity(velocity) / velocity;
// Apply the scale factor and return the result.
let scaled_scroll_mm = SCALE_SCROLL * scale_factor * mm;
if scaled_scroll_mm.is_infinite() || scaled_scroll_mm.is_nan() {
self.metrics_logger.log_error(
InputPipelineErrorMetricDimensionEvent::PointerSensorScaleHandlerScaledScrollInvalid,
std::format!(
"skipped scroll; scaled scroll of {:?} is infinite or NaN.",
scaled_scroll_mm,
));
return Some(mouse_binding::WheelDelta {
raw_data: mouse_binding::RawWheelDelta::Millimeters(mm),
physical_pixel: Some(SCALE_SCROLL * mm),
});
}
Some(mouse_binding::WheelDelta {
raw_data: mouse_binding::RawWheelDelta::Millimeters(mm),
physical_pixel: Some(scaled_scroll_mm),
})
}
}
}
}
#[cfg(test)]
mod tests {
use {
super::*,
crate::input_handler::InputHandler,
crate::testing_utilities,
assert_matches::assert_matches,
fuchsia_async as fasync, fuchsia_inspect, fuchsia_zircon as zx,
maplit::hashset,
std::cell::Cell,
std::ops::Add,
test_util::{assert_gt, assert_lt, assert_near},
};
const COUNTS_PER_MM: f32 = 12.0;
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,
counts_per_mm: COUNTS_PER_MM as u32,
});
// 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,
}
}
#[fuchsia::test]
fn pointer_sensor_scale_handler_initialized_with_inspect_node() {
let inspector = fuchsia_inspect::Inspector::default();
let fake_handlers_node = inspector.root().create_child("input_handlers_node");
let _handler =
PointerSensorScaleHandler::new(&fake_handlers_node, metrics::MetricsLogger::default());
diagnostics_assertions::assert_data_tree!(inspector, root: {
input_handlers_node: {
pointer_sensor_scale_handler: {
events_received_count: 0u64,
events_handled_count: 0u64,
last_received_timestamp_ns: 0u64,
"fuchsia.inspect.Health": {
status: "STARTING_UP",
// Timestamp value is unpredictable and not relevant in this context,
// so we only assert that the property is present.
start_timestamp_nanos: diagnostics_assertions::AnyProperty
},
}
}
});
}
#[fasync::run_singlethreaded(test)]
async fn pointer_sensor_scale_handler_inspect_counts_events() {
let inspector = fuchsia_inspect::Inspector::default();
let fake_handlers_node = inspector.root().create_child("input_handlers_node");
let handler =
PointerSensorScaleHandler::new(&fake_handlers_node, metrics::MetricsLogger::default());
let event_time1 = zx::Time::get_monotonic();
let event_time2 = event_time1.add(fuchsia_zircon::Duration::from_micros(1));
let event_time3 = event_time2.add(fuchsia_zircon::Duration::from_micros(1));
let input_events = vec![
testing_utilities::create_mouse_event(
mouse_binding::MouseLocation::Absolute(Position { x: 0.0, y: 0.0 }),
None, /* wheel_delta_v */
None, /* wheel_delta_h */
None, /* is_precision_scroll */
mouse_binding::MousePhase::Wheel,
hashset! {},
hashset! {},
event_time1,
&DEVICE_DESCRIPTOR,
),
testing_utilities::create_mouse_event(
mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
millimeters: Position { x: 1.5 / COUNTS_PER_MM, y: 4.5 / COUNTS_PER_MM },
}),
None, /* wheel_delta_v */
None, /* wheel_delta_h */
None, /* is_precision_scroll */
mouse_binding::MousePhase::Move,
hashset! {},
hashset! {},
event_time2,
&DEVICE_DESCRIPTOR,
),
// Should not count non-mouse input events.
testing_utilities::create_fake_input_event(event_time2),
// Should not count received events that have already been handled.
testing_utilities::create_mouse_event_with_handled(
mouse_binding::MouseLocation::Absolute(Position { x: 0.0, y: 0.0 }),
None, /* wheel_delta_v */
None, /* wheel_delta_h */
None, /* is_precision_scroll */
mouse_binding::MousePhase::Wheel,
hashset! {},
hashset! {},
event_time3,
&DEVICE_DESCRIPTOR,
input_device::Handled::Yes,
),
];
for input_event in input_events {
let _ = handler.clone().handle_input_event(input_event).await;
}
let last_received_event_time: u64 = event_time2.into_nanos().try_into().unwrap();
diagnostics_assertions::assert_data_tree!(inspector, root: {
input_handlers_node: {
pointer_sensor_scale_handler: {
events_received_count: 2u64,
events_handled_count: 0u64,
last_received_timestamp_ns: last_received_event_time,
"fuchsia.inspect.Health": {
status: "STARTING_UP",
// Timestamp value is unpredictable and not relevant in this context,
// so we only assert that the property is present.
start_timestamp_nanos: diagnostics_assertions::AnyProperty
},
}
}
});
}
// 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!(
PointerSensorScaleHandler::scale_low_speed(MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC),
PointerSensorScaleHandler::scale_medium_speed(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!(
PointerSensorScaleHandler::scale_medium_speed(MEDIUM_SPEED_RANGE_END_MM_PER_SEC),
PointerSensorScaleHandler::scale_high_speed(MEDIUM_SPEED_RANGE_END_MM_PER_SEC),
SCALE_EPSILON
);
}
}
mod motion_scaling_mm {
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_mm(
Position { x: count as f32 / COUNTS_PER_MM, y: 0.0 },
duration,
)
.await;
tracing::error!("{}, {}", count, scaled_count.x);
}
}
async fn get_scaled_motion_mm(movement_mm: Position, duration: zx::Duration) -> Position {
let inspector = fuchsia_inspect::Inspector::default();
let test_node = inspector.root().create_child("test_node");
let handler =
PointerSensorScaleHandler::new(&test_node, metrics::MetricsLogger::default());
// 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! {},
is_precision_scroll: None,
}),
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 { millimeters: movement_mm },
),
wheel_delta_v: None,
wheel_delta_h: None,
phase: mouse_binding::MousePhase::Move,
affected_buttons: hashset! {},
pressed_buttons: hashset! {},
is_precision_scroll: None,
}),
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 {
millimeters: movement_mm,
}),
..
}),
..
} = transformed_events[0]
{
movement_mm
} else {
unreachable!()
}
}
fn velocity_to_mm(velocity_mm_per_sec: f32, duration: zx::Duration) -> f32 {
velocity_mm_per_sec * (duration.into_nanos() as f32 / 1E9)
}
#[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_MM: f32 = 1.0 / COUNTS_PER_MM;
const MOTION_B_MM: f32 = 2.0 / COUNTS_PER_MM;
assert_lt!(
MOTION_B_MM,
velocity_to_mm(MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC, TICK_DURATION)
);
let scaled_a =
get_scaled_motion_mm(Position { x: MOTION_A_MM, y: 0.0 }, TICK_DURATION).await;
let scaled_b =
get_scaled_motion_mm(Position { x: MOTION_B_MM, 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_MM: f32 = 1.0 / COUNTS_PER_MM;
const MOTION_B_MM: f32 = 2.0 / COUNTS_PER_MM;
assert_lt!(
MOTION_B_MM,
velocity_to_mm(MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC, TICK_DURATION)
);
let scaled_a =
get_scaled_motion_mm(Position { x: 0.0, y: MOTION_A_MM }, TICK_DURATION).await;
let scaled_b =
get_scaled_motion_mm(Position { x: 0.0, y: MOTION_B_MM }, 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_MM: f32 = 1.0 / COUNTS_PER_MM;
assert_lt!(
MOTION_MM,
velocity_to_mm(MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC, TICK_DURATION)
);
let scaled =
get_scaled_motion_mm(Position { x: MOTION_MM, y: MOTION_MM }, 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_MM: f32 = 7.0 / COUNTS_PER_MM;
const MOTION_B_MM: f32 = 14.0 / COUNTS_PER_MM;
assert_gt!(
MOTION_A_MM,
velocity_to_mm(MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC, TICK_DURATION)
);
assert_lt!(
MOTION_B_MM,
velocity_to_mm(MEDIUM_SPEED_RANGE_END_MM_PER_SEC, TICK_DURATION)
);
let scaled_a =
get_scaled_motion_mm(Position { x: MOTION_A_MM, y: 0.0 }, TICK_DURATION).await;
let scaled_b =
get_scaled_motion_mm(Position { x: MOTION_B_MM, 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_MM: f32 = 16.0 / COUNTS_PER_MM;
const MOTION_B_MM: f32 = 20.0 / COUNTS_PER_MM;
assert_gt!(
MOTION_A_MM,
velocity_to_mm(MEDIUM_SPEED_RANGE_END_MM_PER_SEC, TICK_DURATION)
);
let scaled_a =
get_scaled_motion_mm(Position { x: MOTION_A_MM, y: 0.0 }, TICK_DURATION).await;
let scaled_b =
get_scaled_motion_mm(Position { x: MOTION_B_MM, 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_mm(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_mm(
Position { x: 1.0 / COUNTS_PER_MM, y: 0.0 },
zx::Duration::from_millis(0),
)
.await;
}
}
mod scroll_scaling_tick {
use {super::*, test_case::test_case};
#[test_case(mouse_binding::MouseEvent {
location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
millimeters: Position::zero(),
}),
wheel_delta_v: Some(mouse_binding::WheelDelta {
raw_data: mouse_binding::RawWheelDelta::Ticks(1),
physical_pixel: None,
}),
wheel_delta_h: None,
phase: mouse_binding::MousePhase::Wheel,
affected_buttons: hashset! {},
pressed_buttons: hashset! {},
is_precision_scroll: None,
} => (Some(PIXELS_PER_TICK), None); "v")]
#[test_case(mouse_binding::MouseEvent {
location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
millimeters: Position::zero(),
}),
wheel_delta_v: None,
wheel_delta_h: Some(mouse_binding::WheelDelta {
raw_data: mouse_binding::RawWheelDelta::Ticks(1),
physical_pixel: None,
}),
phase: mouse_binding::MousePhase::Wheel,
affected_buttons: hashset! {},
pressed_buttons: hashset! {},
is_precision_scroll: None,
} => (None, Some(PIXELS_PER_TICK)); "h")]
#[fuchsia::test(allow_stalls = false)]
async fn scaled(event: mouse_binding::MouseEvent) -> (Option<f32>, Option<f32>) {
let inspector = fuchsia_inspect::Inspector::default();
let test_node = inspector.root().create_child("test_node");
let handler =
PointerSensorScaleHandler::new(&test_node, metrics::MetricsLogger::default());
let unhandled_event = make_unhandled_input_event(event);
let events = handler.clone().handle_unhandled_input_event(unhandled_event).await;
assert_matches!(
events.as_slice(),
[input_device::InputEvent {
device_event: input_device::InputDeviceEvent::Mouse(
mouse_binding::MouseEvent { .. }
),
..
}]
);
if let input_device::InputEvent {
device_event:
input_device::InputDeviceEvent::Mouse(mouse_binding::MouseEvent {
wheel_delta_v,
wheel_delta_h,
..
}),
..
} = events[0].clone()
{
match (wheel_delta_v, wheel_delta_h) {
(None, None) => return (None, None),
(None, Some(delta_h)) => return (None, delta_h.physical_pixel),
(Some(delta_v), None) => return (delta_v.physical_pixel, None),
(Some(delta_v), Some(delta_h)) => {
return (delta_v.physical_pixel, delta_h.physical_pixel)
}
}
} else {
unreachable!();
}
}
}
mod scroll_scaling_mm {
use {super::*, pretty_assertions::assert_eq};
async fn get_scaled_scroll_mm(
wheel_delta_v_mm: Option<f32>,
wheel_delta_h_mm: Option<f32>,
duration: zx::Duration,
) -> (Option<mouse_binding::WheelDelta>, Option<mouse_binding::WheelDelta>) {
let inspector = fuchsia_inspect::Inspector::default();
let test_node = inspector.root().create_child("test_node");
let handler =
PointerSensorScaleHandler::new(&test_node, metrics::MetricsLogger::default());
// 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: Some(mouse_binding::WheelDelta {
raw_data: mouse_binding::RawWheelDelta::Millimeters(1.0),
physical_pixel: None,
}),
wheel_delta_h: None,
phase: mouse_binding::MousePhase::Wheel,
affected_buttons: hashset! {},
pressed_buttons: hashset! {},
is_precision_scroll: None,
}),
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(Default::default()),
wheel_delta_v: match wheel_delta_v_mm {
None => None,
Some(delta) => Some(mouse_binding::WheelDelta {
raw_data: mouse_binding::RawWheelDelta::Millimeters(delta),
physical_pixel: None,
}),
},
wheel_delta_h: match wheel_delta_h_mm {
None => None,
Some(delta) => Some(mouse_binding::WheelDelta {
raw_data: mouse_binding::RawWheelDelta::Millimeters(delta),
physical_pixel: None,
}),
},
phase: mouse_binding::MousePhase::Wheel,
affected_buttons: hashset! {},
pressed_buttons: hashset! {},
is_precision_scroll: None,
}),
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;
assert_eq!(transformed_events.len(), 1);
if let input_device::InputEvent {
device_event:
input_device::InputDeviceEvent::Mouse(mouse_binding::MouseEvent {
wheel_delta_v: delta_v,
wheel_delta_h: delta_h,
..
}),
..
} = transformed_events[0].clone()
{
return (delta_v, delta_h);
} else {
unreachable!()
}
}
fn velocity_to_mm(velocity_mm_per_sec: f32, duration: zx::Duration) -> f32 {
velocity_mm_per_sec * (duration.into_nanos() as f32 / 1E9)
}
#[fuchsia::test(allow_stalls = false)]
async fn low_speed_horizontal_scroll_scales_linearly() {
const TICK_DURATION: zx::Duration = zx::Duration::from_millis(8);
const MOTION_A_MM: f32 = 1.0 / COUNTS_PER_MM;
const MOTION_B_MM: f32 = 2.0 / COUNTS_PER_MM;
assert_lt!(
MOTION_B_MM,
velocity_to_mm(MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC, TICK_DURATION)
);
let (_, scaled_a_h) =
get_scaled_scroll_mm(None, Some(MOTION_A_MM), TICK_DURATION).await;
let (_, scaled_b_h) =
get_scaled_scroll_mm(None, Some(MOTION_B_MM), TICK_DURATION).await;
match (scaled_a_h, scaled_b_h) {
(Some(a_h), Some(b_h)) => {
assert_ne!(a_h.physical_pixel, None);
assert_ne!(b_h.physical_pixel, None);
assert_ne!(a_h.physical_pixel.unwrap(), 0.0);
assert_ne!(b_h.physical_pixel.unwrap(), 0.0);
assert_near!(
b_h.physical_pixel.unwrap() / a_h.physical_pixel.unwrap(),
2.0,
SCALE_EPSILON
);
}
_ => {
panic!("wheel delta is none");
}
}
}
#[fuchsia::test(allow_stalls = false)]
async fn low_speed_vertical_scroll_scales_linearly() {
const TICK_DURATION: zx::Duration = zx::Duration::from_millis(8);
const MOTION_A_MM: f32 = 1.0 / COUNTS_PER_MM;
const MOTION_B_MM: f32 = 2.0 / COUNTS_PER_MM;
assert_lt!(
MOTION_B_MM,
velocity_to_mm(MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC, TICK_DURATION)
);
let (scaled_a_v, _) =
get_scaled_scroll_mm(Some(MOTION_A_MM), None, TICK_DURATION).await;
let (scaled_b_v, _) =
get_scaled_scroll_mm(Some(MOTION_B_MM), None, TICK_DURATION).await;
match (scaled_a_v, scaled_b_v) {
(Some(a_v), Some(b_v)) => {
assert_ne!(a_v.physical_pixel, None);
assert_ne!(b_v.physical_pixel, None);
assert_near!(
b_v.physical_pixel.unwrap() / a_v.physical_pixel.unwrap(),
2.0,
SCALE_EPSILON
);
}
_ => {
panic!("wheel delta is none");
}
}
}
#[fuchsia::test(allow_stalls = false)]
async fn medium_speed_horizontal_scroll_scales_quadratically() {
const TICK_DURATION: zx::Duration = zx::Duration::from_millis(8);
const MOTION_A_MM: f32 = 7.0 / COUNTS_PER_MM;
const MOTION_B_MM: f32 = 14.0 / COUNTS_PER_MM;
assert_gt!(
MOTION_A_MM,
velocity_to_mm(MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC, TICK_DURATION)
);
assert_lt!(
MOTION_B_MM,
velocity_to_mm(MEDIUM_SPEED_RANGE_END_MM_PER_SEC, TICK_DURATION)
);
let (_, scaled_a_h) =
get_scaled_scroll_mm(None, Some(MOTION_A_MM), TICK_DURATION).await;
let (_, scaled_b_h) =
get_scaled_scroll_mm(None, Some(MOTION_B_MM), TICK_DURATION).await;
match (scaled_a_h, scaled_b_h) {
(Some(a_h), Some(b_h)) => {
assert_ne!(a_h.physical_pixel, None);
assert_ne!(b_h.physical_pixel, None);
assert_ne!(a_h.physical_pixel.unwrap(), 0.0);
assert_ne!(b_h.physical_pixel.unwrap(), 0.0);
assert_near!(
b_h.physical_pixel.unwrap() / a_h.physical_pixel.unwrap(),
4.0,
SCALE_EPSILON
);
}
_ => {
panic!("wheel delta is none");
}
}
}
#[fuchsia::test(allow_stalls = false)]
async fn medium_speed_vertical_scroll_scales_quadratically() {
const TICK_DURATION: zx::Duration = zx::Duration::from_millis(8);
const MOTION_A_MM: f32 = 7.0 / COUNTS_PER_MM;
const MOTION_B_MM: f32 = 14.0 / COUNTS_PER_MM;
assert_gt!(
MOTION_A_MM,
velocity_to_mm(MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC, TICK_DURATION)
);
assert_lt!(
MOTION_B_MM,
velocity_to_mm(MEDIUM_SPEED_RANGE_END_MM_PER_SEC, TICK_DURATION)
);
let (scaled_a_v, _) =
get_scaled_scroll_mm(Some(MOTION_A_MM), None, TICK_DURATION).await;
let (scaled_b_v, _) =
get_scaled_scroll_mm(Some(MOTION_B_MM), None, TICK_DURATION).await;
match (scaled_a_v, scaled_b_v) {
(Some(a_v), Some(b_v)) => {
assert_ne!(a_v.physical_pixel, None);
assert_ne!(b_v.physical_pixel, None);
assert_near!(
b_v.physical_pixel.unwrap() / a_v.physical_pixel.unwrap(),
4.0,
SCALE_EPSILON
);
}
_ => {
panic!("wheel delta is none");
}
}
}
#[fuchsia::test(allow_stalls = false)]
async fn high_speed_horizontal_scroll_scaling_is_inreasing() {
const TICK_DURATION: zx::Duration = zx::Duration::from_millis(8);
const MOTION_A_MM: f32 = 16.0 / COUNTS_PER_MM;
const MOTION_B_MM: f32 = 20.0 / COUNTS_PER_MM;
assert_gt!(
MOTION_A_MM,
velocity_to_mm(MEDIUM_SPEED_RANGE_END_MM_PER_SEC, TICK_DURATION)
);
let (_, scaled_a_h) =
get_scaled_scroll_mm(None, Some(MOTION_A_MM), TICK_DURATION).await;
let (_, scaled_b_h) =
get_scaled_scroll_mm(None, Some(MOTION_B_MM), TICK_DURATION).await;
match (scaled_a_h, scaled_b_h) {
(Some(a_h), Some(b_h)) => {
assert_ne!(a_h.physical_pixel, None);
assert_ne!(b_h.physical_pixel, None);
assert_ne!(a_h.physical_pixel.unwrap(), 0.0);
assert_ne!(b_h.physical_pixel.unwrap(), 0.0);
assert_gt!(b_h.physical_pixel.unwrap(), a_h.physical_pixel.unwrap());
}
_ => {
panic!("wheel delta is none");
}
}
}
#[fuchsia::test(allow_stalls = false)]
async fn high_speed_vertical_scroll_scaling_is_inreasing() {
const TICK_DURATION: zx::Duration = zx::Duration::from_millis(8);
const MOTION_A_MM: f32 = 16.0 / COUNTS_PER_MM;
const MOTION_B_MM: f32 = 20.0 / COUNTS_PER_MM;
assert_gt!(
MOTION_A_MM,
velocity_to_mm(MEDIUM_SPEED_RANGE_END_MM_PER_SEC, TICK_DURATION)
);
let (scaled_a_v, _) =
get_scaled_scroll_mm(Some(MOTION_A_MM), None, TICK_DURATION).await;
let (scaled_b_v, _) =
get_scaled_scroll_mm(Some(MOTION_B_MM), None, TICK_DURATION).await;
match (scaled_a_v, scaled_b_v) {
(Some(a_v), Some(b_v)) => {
assert_ne!(a_v.physical_pixel, None);
assert_ne!(b_v.physical_pixel, None);
assert_gt!(b_v.physical_pixel.unwrap(), a_v.physical_pixel.unwrap());
}
_ => {
panic!("wheel delta is none");
}
}
}
}
mod metadata_preservation {
use {super::*, test_case::test_case};
#[test_case(mouse_binding::MouseEvent {
location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
millimeters: Position { x: 1.5 / COUNTS_PER_MM, y: 4.5 / COUNTS_PER_MM },
}),
wheel_delta_v: None,
wheel_delta_h: None,
phase: mouse_binding::MousePhase::Move,
affected_buttons: hashset! {},
pressed_buttons: hashset! {},
is_precision_scroll: None,
}; "move event")]
#[test_case(mouse_binding::MouseEvent {
location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
millimeters: Position::zero(),
}),
wheel_delta_v: Some(mouse_binding::WheelDelta {
raw_data: mouse_binding::RawWheelDelta::Ticks(1),
physical_pixel: None,
}),
wheel_delta_h: None,
phase: mouse_binding::MousePhase::Wheel,
affected_buttons: hashset! {},
pressed_buttons: hashset! {},
is_precision_scroll: None,
}; "wheel event")]
#[fuchsia::test(allow_stalls = false)]
async fn does_not_consume_event(event: mouse_binding::MouseEvent) {
let inspector = fuchsia_inspect::Inspector::default();
let test_node = inspector.root().create_child("test_node");
let handler =
PointerSensorScaleHandler::new(&test_node, metrics::MetricsLogger::default());
let input_event = make_unhandled_input_event(event);
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.
#[test_case(mouse_binding::MouseEvent {
location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
millimeters: Position { x: 1.5 / COUNTS_PER_MM, y: 4.5 / COUNTS_PER_MM },
}),
wheel_delta_v: None,
wheel_delta_h: None,
phase: mouse_binding::MousePhase::Move,
affected_buttons: hashset! {},
pressed_buttons: hashset! {},
is_precision_scroll: None,
}; "move event")]
#[test_case(mouse_binding::MouseEvent {
location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
millimeters: Position::zero(),
}),
wheel_delta_v: Some(mouse_binding::WheelDelta {
raw_data: mouse_binding::RawWheelDelta::Ticks(1),
physical_pixel: None,
}),
wheel_delta_h: None,
phase: mouse_binding::MousePhase::Wheel,
affected_buttons: hashset! {},
pressed_buttons: hashset! {},
is_precision_scroll: None,
}; "wheel event")]
#[fuchsia::test(allow_stalls = false)]
async fn preserves_event_time(event: mouse_binding::MouseEvent) {
let inspector = fuchsia_inspect::Inspector::default();
let test_node = inspector.root().create_child("test_node");
let handler =
PointerSensorScaleHandler::new(&test_node, metrics::MetricsLogger::default());
let mut input_event = make_unhandled_input_event(event);
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, .. }]
);
}
#[test_case(
mouse_binding::MouseEvent {
location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
millimeters: Position::zero(),
}),
wheel_delta_v: Some(mouse_binding::WheelDelta {
raw_data: mouse_binding::RawWheelDelta::Ticks(1),
physical_pixel: Some(1.0),
}),
wheel_delta_h: None,
phase: mouse_binding::MousePhase::Wheel,
affected_buttons: hashset! {},
pressed_buttons: hashset! {},
is_precision_scroll: Some(mouse_binding::PrecisionScroll::No),
} => matches input_device::InputEvent {
device_event: input_device::InputDeviceEvent::Mouse(mouse_binding::MouseEvent {
is_precision_scroll: Some(mouse_binding::PrecisionScroll::No),
..
}),
..
}; "no")]
#[test_case(
mouse_binding::MouseEvent {
location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
millimeters: Position::zero(),
}),
wheel_delta_v: Some(mouse_binding::WheelDelta {
raw_data: mouse_binding::RawWheelDelta::Ticks(1),
physical_pixel: Some(1.0),
}),
wheel_delta_h: None,
phase: mouse_binding::MousePhase::Wheel,
affected_buttons: hashset! {},
pressed_buttons: hashset! {},
is_precision_scroll: Some(mouse_binding::PrecisionScroll::Yes),
} => matches input_device::InputEvent {
device_event: input_device::InputDeviceEvent::Mouse(mouse_binding::MouseEvent {
is_precision_scroll: Some(mouse_binding::PrecisionScroll::Yes),
..
}),
..
}; "yes")]
#[fuchsia::test(allow_stalls = false)]
async fn preserves_is_precision_scroll(
event: mouse_binding::MouseEvent,
) -> input_device::InputEvent {
let inspector = fuchsia_inspect::Inspector::default();
let test_node = inspector.root().create_child("test_node");
let handler =
PointerSensorScaleHandler::new(&test_node, metrics::MetricsLogger::default());
let input_event = make_unhandled_input_event(event);
handler.clone().handle_unhandled_input_event(input_event).await[0].clone()
}
}
}