use crate::input_handler::{InputHandlerStatus, UnhandledInputHandler};
use crate::utils::Position;
use crate::{input_device, metrics, mouse_binding};
use async_trait::async_trait;
use fuchsia_inspect::health::Reporter;
use metrics_registry::*;
use std::cell::RefCell;
use std::num::FpCategory;
use std::rc::Rc;
pub struct PointerSensorScaleHandler {
mutable_state: RefCell<MutableState>,
pub inspect_status: InputHandlerStatus,
metrics_logger: metrics::MetricsLogger,
}
struct MutableState {
last_move_timestamp: Option<zx::MonotonicInstant>,
last_scroll_timestamp: Option<zx::MonotonicInstant>,
}
const PIXELS_PER_TICK: f32 = 120.0;
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,
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);
}
}
const MIN_PLAUSIBLE_EVENT_DELAY: zx::MonotonicDuration = zx::MonotonicDuration::from_micros(100);
const MAX_PLAUSIBLE_EVENT_DELAY: zx::MonotonicDuration = zx::MonotonicDuration::from_millis(50);
const MAX_SENSOR_COUNTS_PER_INCH: f32 = 20_000.0; 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;
const MEDIUM_SPEED_RANGE_BEGIN_MM_PER_SEC: f32 = 32.0;
const MEDIUM_SPEED_RANGE_END_MM_PER_SEC: f32 = 150.0;
const NUMBNESS: f32 = 37.5;
impl PointerSensorScaleHandler {
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",
false,
);
Rc::new(Self {
mutable_state: RefCell::new(MutableState {
last_move_timestamp: None,
last_scroll_timestamp: None,
}),
inspect_status,
metrics_logger,
})
}
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
}
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
}
fn scale_high_speed(movement_mm_per_sec: f32) -> f32 {
const LINEAR_SCALE_FACTOR: f32 = 2.0 * (MEDIUM_SPEED_RANGE_END_MM_PER_SEC / NUMBNESS);
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;
LINEAR_SCALE_FACTOR * movement_mm_per_sec + OFFSET
}
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)
}
}
fn scale_motion(&self, movement_mm: Position, event_time: zx::MonotonicInstant) -> Position {
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;
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 {
return movement_mm;
}
let scale_factor = Self::scale_euclidean_velocity(euclidean_velocity) / euclidean_velocity;
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) => {
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,
}
}
fn scale_scroll(
&self,
wheel_delta: Option<mouse_binding::WheelDelta>,
event_time: zx::MonotonicInstant,
) -> 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),
..
}) => {
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 {
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;
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::*;
use crate::input_handler::InputHandler;
use crate::testing_utilities;
use assert_matches::assert_matches;
use maplit::hashset;
use std::cell::Cell;
use std::ops::Add;
use test_util::{assert_gt, assert_lt, assert_near};
use {fuchsia_async as fasync, fuchsia_inspect};
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,
});
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::MonotonicInstant::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",
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::MonotonicInstant::get();
let event_time2 = event_time1.add(zx::MonotonicDuration::from_micros(1));
let event_time3 = event_time2.add(zx::MonotonicDuration::from_micros(1));
let input_events = vec![
testing_utilities::create_mouse_event(
mouse_binding::MouseLocation::Absolute(Position { x: 0.0, y: 0.0 }),
None, None, None, 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, None, None, mouse_binding::MousePhase::Move,
hashset! {},
hashset! {},
event_time2,
&DEVICE_DESCRIPTOR,
),
testing_utilities::create_fake_input_event(event_time2),
testing_utilities::create_mouse_event_with_handled(
mouse_binding::MouseLocation::Absolute(Position { x: 0.0, y: 0.0 }),
None, None, None, 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",
start_timestamp_nanos: diagnostics_assertions::AnyProperty
},
}
}
});
}
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
);
}
#[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::MonotonicDuration::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::MonotonicDuration,
) -> 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());
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::MonotonicInstant::from_nanos(0),
trace_id: None,
};
handler.clone().handle_unhandled_input_event(input_event).await;
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::MonotonicInstant::from_nanos(duration.into_nanos()),
trace_id: None,
};
let transformed_events =
handler.clone().handle_unhandled_input_event(input_event).await;
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 { .. }
),
..
}
),
..
}]
);
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::MonotonicDuration) -> 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::MonotonicDuration = zx::MonotonicDuration::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::MonotonicDuration = zx::MonotonicDuration::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::MonotonicDuration = zx::MonotonicDuration::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::MonotonicDuration = zx::MonotonicDuration::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);
}
#[fuchsia::test(allow_stalls = false)]
async fn high_speed_motion_scaling_is_increasing() {
const TICK_DURATION: zx::MonotonicDuration = zx::MonotonicDuration::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::MonotonicDuration = zx::MonotonicDuration::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::MonotonicDuration::from_millis(0),
)
.await;
}
}
mod scroll_scaling_tick {
use super::*;
use 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::*;
use pretty_assertions::assert_eq;
async fn get_scaled_scroll_mm(
wheel_delta_v_mm: Option<f32>,
wheel_delta_h_mm: Option<f32>,
duration: zx::MonotonicDuration,
) -> (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());
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::MonotonicInstant::from_nanos(0),
trace_id: None,
};
handler.clone().handle_unhandled_input_event(input_event).await;
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::MonotonicInstant::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::MonotonicDuration) -> 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::MonotonicDuration = zx::MonotonicDuration::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::MonotonicDuration = zx::MonotonicDuration::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::MonotonicDuration = zx::MonotonicDuration::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::MonotonicDuration = zx::MonotonicDuration::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::MonotonicDuration = zx::MonotonicDuration::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::MonotonicDuration = zx::MonotonicDuration::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::*;
use 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, .. }]
);
}
#[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::MonotonicInstant = zx::MonotonicInstant::from_nanos(42);
input_event.event_time = EVENT_TIME;
let events = handler.clone().handle_unhandled_input_event(input_event).await;
assert_eq!(events.len(), 1, "{events:?} should be length 1");
assert_eq!(events[0].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()
}
}
}