blob: 787b49dd6b34e2205dc97e5c68cfee4f938dabde [file] [log] [blame]
// Copyright 2021 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_debug, fx_log_warn},
fuchsia_zircon as zx,
std::{
cell::{Cell, RefCell},
collections::HashSet,
convert::{From, TryFrom},
rc::Rc,
},
};
/// Represents an InputEvent that is relevant to this handler.
/// Notably excludes absolutely positioned pointers, and events
/// not associated with a mouse device.
#[derive(Debug)]
struct RelativeMouseEvent {
displacement: Position, // Change in position since the previous event
phase: mouse_binding::MousePhase,
affected_buttons: HashSet<mouse_binding::MouseButton>,
pressed_buttons: HashSet<mouse_binding::MouseButton>,
mouse_descriptor: mouse_binding::MouseDeviceDescriptor,
event_time: zx::Time,
handled: input_device::Handled,
}
impl TryFrom<input_device::UnhandledInputEvent> for RelativeMouseEvent {
type Error = input_device::UnhandledInputEvent;
fn try_from(
unhandled_input_event: input_device::UnhandledInputEvent,
) -> Result<RelativeMouseEvent, Self::Error> {
match unhandled_input_event {
input_device::UnhandledInputEvent {
device_event:
input_device::InputDeviceEvent::Mouse(mouse_binding::MouseEvent {
location:
mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
counts: position,
..
}),
// Click and drag only interested in motion and button events.
phase:
phase @ mouse_binding::MousePhase::Move
| phase @ mouse_binding::MousePhase::Down
| phase @ mouse_binding::MousePhase::Up,
affected_buttons,
pressed_buttons,
..
}),
device_descriptor: input_device::InputDeviceDescriptor::Mouse(mouse_descriptor),
event_time,
trace_id: _,
} => Ok(RelativeMouseEvent {
displacement: position,
phase,
affected_buttons,
pressed_buttons,
mouse_descriptor,
event_time,
handled: input_device::Handled::No,
}),
_ => Err(unhandled_input_event),
}
}
}
impl From<RelativeMouseEvent> for input_device::InputEvent {
fn from(relative_mouse_event: RelativeMouseEvent) -> input_device::InputEvent {
input_device::InputEvent {
device_event: input_device::InputDeviceEvent::Mouse(mouse_binding::MouseEvent {
location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
counts: relative_mouse_event.displacement,
// TODO(https://fxbug.dev/102567): Implement millimeters.
millimeters: Position::zero(),
}),
wheel_delta_v: None,
wheel_delta_h: None,
phase: relative_mouse_event.phase.into(),
affected_buttons: relative_mouse_event.affected_buttons,
pressed_buttons: relative_mouse_event.pressed_buttons,
}),
device_descriptor: input_device::InputDeviceDescriptor::Mouse(
relative_mouse_event.mouse_descriptor,
),
event_time: relative_mouse_event.event_time,
handled: relative_mouse_event.handled,
trace_id: None,
}
}
}
/// States in the disambiguation process.
///
/// | Current state | Description | Event | Next state |
/// | ---------------- | ----------------------------------- | ---------------- | ---------------- |
/// | NoActiveGesture | Button not actuated | Down | AmbiguousGesture |
/// | NoActiveGesture | (as above) | Move | NoActiveGesture |
/// | NoActiveGesture | (as above) | Up | NoActiveGesture |
/// | AmbiguousGesture | Button actuated, motion < threshold | Down | AmbiguousGesture |
/// | AmbiguousGesture | (as above) | Move < threshold | AmbiguousGesture |
/// | AmbiguousGesture | (as above) | Move > threshold | DragGesture |
/// | AmbiguousGesture | (as above) | Up | ClickGesture |
/// | DragGesture | Button actuated, motion > threshold | Down | DragGesture |
/// | DragGesture | (as above) | Move | DragGesture |
/// | DragGesture | (as above) | Up | NoActiveGesture |
///
/// Notes
/// 1. "Move < threshold" is short-hand for "a Move event after which the total displacement
/// remains below the threshold for distinguishing a click from a drag". Similarly for
/// "Move > threshold". Said differently, the comparison against the threshold is for
/// the _total displacement_, not just the displacement in the current event.
/// 2. The `ClickGesture` state is omitted in the table above, as it is functionally
/// identical to the `NoActiveGesture` state; it (`ClickGesture`) exists just to make
/// state transition logs easy to read.
enum HandlerState {
NoActiveGesture,
AmbiguousGesture {
delta_x: f32, // Net horizontal displacement since a button was depressed
delta_y: f32, // Net vertical displacement since a button was depressed
// Events buffered pending disambiguation of click vs. drag
// TODO(fxbug.dev/89727): Limit the size of this buffer.
buffered_events: Vec<RelativeMouseEvent>,
},
ClickGesture,
DragGesture {
// Net horizontal displacement since a button was depressed. Tracked
// for debugging purposes.
delta_x: f32,
// Net vertical displacement since a button was depressed. Tracked
// for debugging purposes.
delta_y: f32,
},
LogicError,
}
/// Names for the `HandlerState`s. Exists to support the state machine debug log.
#[derive(Debug, PartialEq)]
enum HandlerStateName {
NoActiveGesture,
AmbiguousGesture,
ClickGesture,
DragGesture,
LogicError,
}
impl HandlerState {
fn get_state_name(&self) -> HandlerStateName {
match self {
Self::NoActiveGesture => HandlerStateName::NoActiveGesture,
Self::AmbiguousGesture { .. } => HandlerStateName::AmbiguousGesture,
Self::ClickGesture => HandlerStateName::ClickGesture,
Self::DragGesture { .. } => HandlerStateName::DragGesture,
Self::LogicError { .. } => HandlerStateName::LogicError,
}
}
}
/// Improves the disambiguation of click vs. drag events at upper layers
/// by suppressing spurious motion that happens when clicking on a trackpad.
///
// TODO(fxbug.dev/89726) Define semantics with multiple mouse buttons.
// TODO(fxbug.dev/89724) Define semantics with multiple mouse/trackpad devices.
// TODO(fxbug.dev/89722) Process trackpad events instead of mouse events.
// TODO(fxbug.dev/90512) Switch to a gesture arena.
pub struct ClickDragHandler {
state: RefCell<HandlerState>,
// The time at which we entered the current state. Tracked
// for debugging purposes.
entry_time: Cell<std::time::Instant>,
// The number of events processed in the the current state. Tracked
// for debugging purposes.
event_count: Cell<u64>,
// Threshold Euclidean distance at which an ambiguous gesture
// is resolved to be a drag.
click_to_drag_threshold: f32,
}
#[async_trait(?Send)]
impl UnhandledInputHandler for ClickDragHandler {
async fn handle_unhandled_input_event(
self: Rc<Self>,
unhandled_input_event: input_device::UnhandledInputEvent,
) -> Vec<input_device::InputEvent> {
match RelativeMouseEvent::try_from(unhandled_input_event) {
Ok(converted_event) => self
.process_event(converted_event)
.into_iter()
.map(input_device::InputEvent::from)
.collect(),
Err(original_event) => vec![input_device::InputEvent::from(original_event)],
}
}
}
impl ClickDragHandler {
/// Creates a new [`ClickDragHandler`] that disambiguates clicks vs. drags.
pub fn new(click_to_drag_threshold: f32) -> Rc<Self> {
Rc::new(Self {
state: RefCell::new(HandlerState::NoActiveGesture),
entry_time: Cell::new(std::time::Instant::now()),
event_count: Cell::new(0),
click_to_drag_threshold,
})
}
/// Interprets the `event` in the context of `self.state`.
fn process_event(self: &Rc<Self>, event: RelativeMouseEvent) -> Vec<RelativeMouseEvent> {
// The state machine update logic does not update total displacement
// from Down or Up events, as those are expected to have zero displacement
// (see MouseBinding::process_reports(), which generates those events).
if event.displacement != Position::zero() && event.phase != mouse_binding::MousePhase::Move
{
fx_log_warn!("non-zero displacement in phase {:?}", event.phase)
}
// Take the current state out of `self.state`, and leave a placeholder in
// `self.state`. This avoids the cost of `clone()`-ing `buffered_events`.
let old_state = self.state.replace(HandlerState::LogicError);
let old_state_name = old_state.get_state_name();
let (
new_state,
events_to_propagate,
// The net displacement in the x and y axes at the end of a click or
// drag gesture. `None` in cases where the event did not terminate
// a gesture. Tracked for debugging purposes.
completed_gesture_displacement,
) = match (old_state, event.phase) {
(
HandlerState::NoActiveGesture | HandlerState::ClickGesture,
mouse_binding::MousePhase::Down,
) => (
HandlerState::AmbiguousGesture {
delta_x: 0.0,
delta_y: 0.0,
buffered_events: vec![],
},
vec![event],
None,
),
(
HandlerState::NoActiveGesture | HandlerState::ClickGesture,
mouse_binding::MousePhase::Move,
) => (HandlerState::NoActiveGesture, vec![event], None),
(
HandlerState::NoActiveGesture | HandlerState::ClickGesture,
mouse_binding::MousePhase::Up,
) => {
fx_log_warn!(
"mouse_binding::MousePhase::Up with no button pressed; remaining in NoActiveGesture state"
);
(HandlerState::NoActiveGesture, vec![event], None)
}
(state @ HandlerState::AmbiguousGesture { .. }, mouse_binding::MousePhase::Down) => {
fx_log_warn!(
"mouse_binding::MousePhase::Down with gesture in progress; remaining in AmbiguousGesture state"
);
(state, vec![event], None)
}
(
HandlerState::AmbiguousGesture { delta_x, delta_y, mut buffered_events },
mouse_binding::MousePhase::Move,
) => {
let (delta_x, delta_y) =
(delta_x + event.displacement.x, delta_y + event.displacement.y);
if self.motion_is_in_click_threshold(delta_x, delta_y) {
// The gesture remains ambiguous.
//
// Buffer the event, to avoid having upper layers interpret accidental
// motion as a drag.
(
HandlerState::AmbiguousGesture {
delta_x,
delta_y,
buffered_events: {
// While this could be written in a functional style, doing
// so might make this a best-case O(n) operation. With
// the imperative style, this is amortized O(1).
//
// This is important, because some mice run at 8KHz. If a user
// with an 8KHz mouse held the button for 1 second, O(n) would
// mean copying 8000 * ((8000+1)/2) `RelativeMouseEvent`s.
//
// That works out to 8000 * 4000 * 128 = 4 GB/sec, which could
// make mouse bottlenecked on memory.
buffered_events.push(event);
buffered_events
},
},
vec![],
None,
)
} else {
// The gesture isn't a click, so it must be a drag.
(
HandlerState::DragGesture { delta_x, delta_y },
// Send the buffered events now, so upper layers can start
// responding to the drag.
{
buffered_events.push(event);
buffered_events
},
None,
)
}
}
(
HandlerState::AmbiguousGesture { delta_x, delta_y, buffered_events },
mouse_binding::MousePhase::Up,
) => (
HandlerState::ClickGesture,
buffered_events
.into_iter()
.map(|e| RelativeMouseEvent { handled: input_device::Handled::Yes, ..e })
.chain(std::iter::once(event))
.collect(),
Some((delta_x, delta_y)),
),
(state @ HandlerState::DragGesture { .. }, mouse_binding::MousePhase::Down) => {
fx_log_warn!("mouse_binding::MousePhase::Down with drag in progress; remaining in DragGesture state");
(state, vec![event], None)
}
(HandlerState::DragGesture { delta_x, delta_y }, mouse_binding::MousePhase::Move) => (
HandlerState::DragGesture {
delta_x: delta_x + event.displacement.x,
delta_y: delta_y + event.displacement.y,
},
vec![event],
None,
),
(HandlerState::DragGesture { delta_x, delta_y }, mouse_binding::MousePhase::Up) => {
(HandlerState::NoActiveGesture, vec![event], Some((delta_x, delta_y)))
}
// Wheel events are filtered out on `try_from()`, should not reach to here.
(_, mouse_binding::MousePhase::Wheel) => unreachable!(),
(HandlerState::LogicError, _) => unreachable!(),
};
if new_state.get_state_name() != old_state_name {
let elapsed_time = std::time::Instant::now() - self.entry_time.get();
fx_log_debug!(
"ClickDragHandler: {:?} -> {:?} after {}.{:03} secs, {} events, {:?} displacement",
old_state_name,
new_state.get_state_name(),
elapsed_time.as_secs(),
elapsed_time.subsec_millis(),
self.event_count.get(),
completed_gesture_displacement
);
self.entry_time.set(std::time::Instant::now());
self.event_count.set(0);
} else {
self.event_count.set(self.event_count.get() + 1);
}
self.state.replace(new_state);
events_to_propagate
}
}
impl ClickDragHandler {
/// Returns true iff the Euclidean distance for the displacement indicated
/// by (delta_x, delta_y) is less than the drag threshold.
fn motion_is_in_click_threshold(&self, delta_x: f32, delta_y: f32) -> bool {
(delta_x * delta_x + delta_y * delta_y).sqrt() < self.click_to_drag_threshold
}
}
#[cfg(test)]
mod tests {
use {
super::*, fuchsia_zircon as zx, maplit::hashset, pretty_assertions::assert_eq,
test_case::test_case,
};
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,
});
const CLICK_TO_DRAG_THRESHOLD: f32 = 16.0;
const SMALL_MOTION: f32 = CLICK_TO_DRAG_THRESHOLD * 0.1;
const HALF_MOTION: f32 = CLICK_TO_DRAG_THRESHOLD / 2.0;
const LARGE_MOTION: f32 = CLICK_TO_DRAG_THRESHOLD * 1.2;
const DIAGONAL_LARGE_MOTION: f32 = LARGE_MOTION / 2.0 * std::f32::consts::SQRT_2;
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(allow_stalls = false)]
async fn button_down_is_passed_through_when_no_button_was_previously_clicked() {
let handler = ClickDragHandler::new(CLICK_TO_DRAG_THRESHOLD);
let event = make_unhandled_input_event(mouse_binding::MouseEvent {
location: mouse_binding::MouseLocation::Relative(Default::default()),
wheel_delta_v: None,
wheel_delta_h: None,
phase: mouse_binding::MousePhase::Down,
affected_buttons: hashset! {0},
pressed_buttons: hashset! {0},
});
assert_eq!(
handler.handle_unhandled_input_event(event.clone()).await.as_slice(),
[event.into()]
);
}
#[fuchsia::test(allow_stalls = false)]
async fn move_event_is_passed_through_when_no_button_is_clicked() {
let handler = ClickDragHandler::new(CLICK_TO_DRAG_THRESHOLD);
let event = make_unhandled_input_event(mouse_binding::MouseEvent {
location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
counts: Position { x: SMALL_MOTION, y: SMALL_MOTION },
millimeters: Position::zero(),
}),
wheel_delta_v: None,
wheel_delta_h: None,
phase: mouse_binding::MousePhase::Move,
affected_buttons: hashset! {},
pressed_buttons: hashset! {},
});
assert_eq!(
handler.handle_unhandled_input_event(event.clone()).await.as_slice(),
[event.into()]
);
}
#[fuchsia::test(allow_stalls = false)]
async fn button_down_then_small_motion_yields_no_move_events() {
let handler = ClickDragHandler::new(CLICK_TO_DRAG_THRESHOLD);
let button_down_event = make_unhandled_input_event(mouse_binding::MouseEvent {
location: mouse_binding::MouseLocation::Relative(Default::default()),
wheel_delta_v: None,
wheel_delta_h: None,
phase: mouse_binding::MousePhase::Down,
affected_buttons: hashset! {0},
pressed_buttons: hashset! {0},
});
let move_event = make_unhandled_input_event(mouse_binding::MouseEvent {
location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
counts: Position { x: 0.0, y: SMALL_MOTION },
millimeters: Position::zero(),
}),
wheel_delta_v: None,
wheel_delta_h: None,
phase: mouse_binding::MousePhase::Move,
affected_buttons: hashset! {0},
pressed_buttons: hashset! {0},
});
// Intermediate values verified by
// * button_down_is_passed_through_when_no_button_was_previously_clicked()
handler.clone().handle_unhandled_input_event(button_down_event).await;
assert_eq!(handler.clone().handle_unhandled_input_event(move_event).await.as_slice(), []);
}
#[fuchsia::test(allow_stalls = false)]
async fn button_down_then_small_motion_then_button_up_yields_handled_move_and_unhandled_button_up(
) {
let handler = ClickDragHandler::new(CLICK_TO_DRAG_THRESHOLD);
let button_down_event = make_unhandled_input_event(mouse_binding::MouseEvent {
location: mouse_binding::MouseLocation::Relative(Default::default()),
wheel_delta_v: None,
wheel_delta_h: None,
phase: mouse_binding::MousePhase::Down,
affected_buttons: hashset! {0},
pressed_buttons: hashset! {0},
});
let move_event = make_unhandled_input_event(mouse_binding::MouseEvent {
location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
counts: Position { x: 0.0, y: SMALL_MOTION },
millimeters: Position::zero(),
}),
wheel_delta_v: None,
wheel_delta_h: None,
phase: mouse_binding::MousePhase::Move,
affected_buttons: hashset! {0},
pressed_buttons: hashset! {0},
});
let button_up_event = make_unhandled_input_event(mouse_binding::MouseEvent {
location: mouse_binding::MouseLocation::Relative(Default::default()),
wheel_delta_v: None,
wheel_delta_h: None,
phase: mouse_binding::MousePhase::Up,
affected_buttons: hashset! {0},
pressed_buttons: hashset! {},
});
// Intermediate values verified by
// * button_down_is_passed_through_when_no_button_was_previously_clicked()
// * button_down_then_small_motion_yields_no_move_events()
handler.clone().handle_unhandled_input_event(button_down_event).await;
handler.clone().handle_unhandled_input_event(move_event.clone()).await;
assert_eq!(
handler.clone().handle_unhandled_input_event(button_up_event.clone()).await.as_slice(),
[
input_device::InputEvent {
handled: input_device::Handled::Yes,
..move_event.into()
},
button_up_event.into()
]
);
}
#[test_case(Position { x: -LARGE_MOTION, y: 0.0 }; "leftwards")]
#[test_case(Position { x: LARGE_MOTION, y: 0.0 }; "rightwards")]
#[test_case(Position { x: 0.0, y: -LARGE_MOTION }; "upwards")]
#[test_case(Position { x: 0.0, y: LARGE_MOTION }; "downwards")]
#[test_case(Position { x: DIAGONAL_LARGE_MOTION, y: DIAGONAL_LARGE_MOTION }; "diagonal")]
#[fuchsia::test(allow_stalls = false)]
async fn button_down_then_large_motion_yields_large_motion(position: Position) {
let handler = ClickDragHandler::new(CLICK_TO_DRAG_THRESHOLD);
let button_down_event = make_unhandled_input_event(mouse_binding::MouseEvent {
location: mouse_binding::MouseLocation::Relative(Default::default()),
wheel_delta_v: None,
wheel_delta_h: None,
phase: mouse_binding::MousePhase::Down,
affected_buttons: hashset! {0},
pressed_buttons: hashset! {0},
});
let move_event = make_unhandled_input_event(mouse_binding::MouseEvent {
location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
counts: position,
millimeters: Position::zero(),
}),
wheel_delta_v: None,
wheel_delta_h: None,
phase: mouse_binding::MousePhase::Move,
affected_buttons: hashset! {0},
pressed_buttons: hashset! {0},
});
// Intermediate values verified by
// * button_down_is_passed_through_when_no_button_was_previously_clicked()
handler.clone().handle_unhandled_input_event(button_down_event).await;
pretty_assertions::assert_eq!(
handler.clone().handle_unhandled_input_event(move_event.clone()).await.as_slice(),
[move_event.into()]
);
}
#[fuchsia::test(allow_stalls = false)]
async fn button_up_after_botton_down_and_large_motion_does_not_replay_motion() {
let handler = ClickDragHandler::new(CLICK_TO_DRAG_THRESHOLD);
let button_down_event = make_unhandled_input_event(mouse_binding::MouseEvent {
location: mouse_binding::MouseLocation::Relative(Default::default()),
wheel_delta_v: None,
wheel_delta_h: None,
phase: mouse_binding::MousePhase::Down,
affected_buttons: hashset! {0},
pressed_buttons: hashset! {0},
});
let move_event = make_unhandled_input_event(mouse_binding::MouseEvent {
location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
counts: Position { x: 0.0, y: LARGE_MOTION },
millimeters: Position::zero(),
}),
wheel_delta_v: None,
wheel_delta_h: None,
phase: mouse_binding::MousePhase::Move,
affected_buttons: hashset! {0},
pressed_buttons: hashset! {0},
});
let button_up_event = make_unhandled_input_event(mouse_binding::MouseEvent {
location: mouse_binding::MouseLocation::Relative(Default::default()),
wheel_delta_v: None,
wheel_delta_h: None,
phase: mouse_binding::MousePhase::Up,
affected_buttons: hashset! {0},
pressed_buttons: hashset! {},
});
// Intermediate values verified by
// * button_down_is_passed_through_when_no_button_was_previously_clicked()
// * button_down_then_large_motion_yields_large_motion()
handler.clone().handle_unhandled_input_event(button_down_event).await;
handler.clone().handle_unhandled_input_event(move_event.clone()).await;
assert_eq!(
handler.clone().handle_unhandled_input_event(button_up_event.clone()).await.as_slice(),
[button_up_event.into()]
);
}
#[fuchsia::test(allow_stalls = false)]
async fn button_down_then_two_motions_summing_past_drag_threshold_yields_motions() {
let handler = ClickDragHandler::new(CLICK_TO_DRAG_THRESHOLD);
let button_down_event = make_unhandled_input_event(mouse_binding::MouseEvent {
location: mouse_binding::MouseLocation::Relative(Default::default()),
wheel_delta_v: None,
wheel_delta_h: None,
phase: mouse_binding::MousePhase::Down,
affected_buttons: hashset! {0},
pressed_buttons: hashset! {0},
});
let first_move_event = make_unhandled_input_event(mouse_binding::MouseEvent {
location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
counts: Position { x: 0.0, y: HALF_MOTION },
millimeters: Position::zero(),
}),
wheel_delta_v: None,
wheel_delta_h: None,
phase: mouse_binding::MousePhase::Move,
affected_buttons: hashset! {0},
pressed_buttons: hashset! {0},
});
let second_move_event = make_unhandled_input_event(mouse_binding::MouseEvent {
location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
counts: Position { x: 0.0, y: HALF_MOTION },
millimeters: Position::zero(),
}),
wheel_delta_v: None,
wheel_delta_h: None,
phase: mouse_binding::MousePhase::Move,
affected_buttons: hashset! {0},
pressed_buttons: hashset! {0},
});
// Intermediate values verified by
// * button_down_is_passed_through_when_no_button_was_previously_clicked()
// * button_down_then_small_motion_yields_no_move_events()
handler.clone().handle_unhandled_input_event(button_down_event).await;
handler.clone().handle_unhandled_input_event(first_move_event.clone()).await;
// In the future, it might be necessary to compress the events to save
// memory. If that happens, the logic below will need to validate the total
// displacement, instead of each event.
assert_eq!(
handler
.clone()
.handle_unhandled_input_event(second_move_event.clone())
.await
.as_slice(),
[first_move_event.into(), second_move_event.into()]
);
}
#[fuchsia::test(allow_stalls = false)]
async fn move_events_continue_after_drag_is_recognized() {
let handler = ClickDragHandler::new(CLICK_TO_DRAG_THRESHOLD);
let button_down_event = make_unhandled_input_event(mouse_binding::MouseEvent {
location: mouse_binding::MouseLocation::Relative(Default::default()),
wheel_delta_v: None,
wheel_delta_h: None,
phase: mouse_binding::MousePhase::Down,
affected_buttons: hashset! {0},
pressed_buttons: hashset! {0},
});
let first_move_event = make_unhandled_input_event(mouse_binding::MouseEvent {
location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
counts: Position { x: 0.0, y: HALF_MOTION },
millimeters: Position::zero(),
}),
wheel_delta_v: None,
wheel_delta_h: None,
phase: mouse_binding::MousePhase::Move,
affected_buttons: hashset! {0},
pressed_buttons: hashset! {0},
});
let second_move_event = make_unhandled_input_event(mouse_binding::MouseEvent {
location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
counts: Position { x: 0.0, y: HALF_MOTION },
millimeters: Position::zero(),
}),
wheel_delta_v: None,
wheel_delta_h: None,
phase: mouse_binding::MousePhase::Move,
affected_buttons: hashset! {0},
pressed_buttons: hashset! {0},
});
let third_move_event = make_unhandled_input_event(mouse_binding::MouseEvent {
location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
counts: Position { x: 0.0, y: HALF_MOTION },
millimeters: Position::zero(),
}),
wheel_delta_v: None,
wheel_delta_h: None,
phase: mouse_binding::MousePhase::Move,
affected_buttons: hashset! {0},
pressed_buttons: hashset! {0},
});
// Intermediate values verified by
// * button_down_is_passed_through_when_no_button_was_previously_clicked()
// * button_down_then_two_motions_summing_past_drag_threshold_yields_motions()
handler.clone().handle_unhandled_input_event(button_down_event).await;
handler.clone().handle_unhandled_input_event(first_move_event.clone()).await;
handler.clone().handle_unhandled_input_event(second_move_event.clone()).await;
assert_eq!(
handler.clone().handle_unhandled_input_event(third_move_event.clone()).await.as_slice(),
[third_move_event.into()]
);
}
#[test_case(Position { x: -2.0, y: 0.0 },
Position { x: CLICK_TO_DRAG_THRESHOLD + 1.0, y: 0.0}; "horizontal")]
#[test_case(Position { x: 0.0, y: -2.0 },
Position { x: 0.0, y: CLICK_TO_DRAG_THRESHOLD + 1.0}; "vertical")]
#[fuchsia::test(allow_stalls = false)]
async fn back_and_forth_motion_does_not_spuriously_yield_move_events(
first_motion: Position,
second_motion: Position,
) {
let handler = ClickDragHandler::new(CLICK_TO_DRAG_THRESHOLD);
let button_down_event = make_unhandled_input_event(mouse_binding::MouseEvent {
location: mouse_binding::MouseLocation::Relative(Default::default()),
wheel_delta_v: None,
wheel_delta_h: None,
phase: mouse_binding::MousePhase::Down,
affected_buttons: hashset! {0},
pressed_buttons: hashset! {0},
});
let first_move_event = make_unhandled_input_event(mouse_binding::MouseEvent {
location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
counts: first_motion,
millimeters: Position::zero(),
}),
wheel_delta_v: None,
wheel_delta_h: None,
phase: mouse_binding::MousePhase::Move,
affected_buttons: hashset! {0},
pressed_buttons: hashset! {0},
});
let second_move_event = make_unhandled_input_event(mouse_binding::MouseEvent {
location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
counts: second_motion,
millimeters: Position::zero(),
}),
wheel_delta_v: None,
wheel_delta_h: None,
phase: mouse_binding::MousePhase::Move,
affected_buttons: hashset! {0},
pressed_buttons: hashset! {0},
});
// Intermediate values verified by
// * button_down_is_passed_through_when_no_button_was_previously_clicked()
// * button_down_then_small_motion_yields_no_move_events()
handler.clone().handle_unhandled_input_event(button_down_event).await;
handler.clone().handle_unhandled_input_event(first_move_event.clone()).await;
pretty_assertions::assert_eq!(
handler.clone().handle_unhandled_input_event(second_move_event).await.as_slice(),
[]
);
}
#[fuchsia::test(allow_stalls = false)]
async fn ignore_wheel_events() {
let handler = ClickDragHandler::new(CLICK_TO_DRAG_THRESHOLD);
let wheel_event = make_unhandled_input_event(mouse_binding::MouseEvent {
location: mouse_binding::MouseLocation::Relative(Default::default()),
wheel_delta_v: Some(1),
wheel_delta_h: None,
phase: mouse_binding::MousePhase::Wheel,
affected_buttons: hashset! {},
pressed_buttons: hashset! {},
});
assert_eq!(
handler.clone().handle_unhandled_input_event(wheel_event.clone()).await.as_slice(),
[wheel_event.into()]
);
}
#[fuchsia::test(allow_stalls = false)]
async fn ignore_wheel_events_in_click_drag() {
let handler = ClickDragHandler::new(CLICK_TO_DRAG_THRESHOLD);
let button_down_event = make_unhandled_input_event(mouse_binding::MouseEvent {
location: mouse_binding::MouseLocation::Relative(Default::default()),
wheel_delta_v: None,
wheel_delta_h: None,
phase: mouse_binding::MousePhase::Down,
affected_buttons: hashset! {0},
pressed_buttons: hashset! {0},
});
let move_event = make_unhandled_input_event(mouse_binding::MouseEvent {
location: mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
counts: Position { x: -2.0, y: 0.0 },
millimeters: Position::zero(),
}),
wheel_delta_v: None,
wheel_delta_h: None,
phase: mouse_binding::MousePhase::Move,
affected_buttons: hashset! {0},
pressed_buttons: hashset! {0},
});
let wheel_event = make_unhandled_input_event(mouse_binding::MouseEvent {
location: mouse_binding::MouseLocation::Relative(Default::default()),
wheel_delta_v: Some(1),
wheel_delta_h: None,
phase: mouse_binding::MousePhase::Wheel,
affected_buttons: hashset! {0},
pressed_buttons: hashset! {0},
});
// Intermediate values verified by
// * button_down_is_passed_through_when_no_button_was_previously_clicked()
// * button_down_then_small_motion_yields_no_move_events()
handler.clone().handle_unhandled_input_event(button_down_event).await;
handler.clone().handle_unhandled_input_event(move_event).await;
pretty_assertions::assert_eq!(
handler.clone().handle_unhandled_input_event(wheel_event.clone()).await.as_slice(),
[wheel_event.into()]
);
}
}