| // 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, |
| // TODO(https://fxbug.dev/102567) Use millimeters. |
| counts_per_mm: 1, |
| }); |
| 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()] |
| ); |
| } |
| } |