[input] Adds pointer fusion rust library
This change creates a rust library to fuse raw mouse and touch events
into PointerEvent. This is based off similar implementations found in
Flutter [1],[2] and Chromium [3]. The client of this library will be
AppKit [4] and it's clients.
- This first implementation only fuses mouse events, touch events will
follow in subsequent cls.
[1] - https://github.com/flutter/engine/blob/main/shell/platform/fuchsia/flutter/pointer_delegate.cc
[2] - https://github.com/flutter/engine/blob/main/lib/ui/window/pointer_data_packet_converter.cc
[3] - https://crsrc.org/c/ui/events/fuchsia/pointer_events_handler.cc
[4] - http://cs/fuchsia/src/experiences/session_shells/gazelle/appkit/
Bug: 110099
Test: fx test -o pointer_fusion_tests
Multiply: pointer_fusion_tests
Change-Id: Id3dc1d8a46601a04fa16b1c057816de91befd7f4
Reviewed-on: https://fuchsia-review.googlesource.com/c/experiences/+/731044
Commit-Queue: Sanjay Chouksey <sanjayc@google.com>
Reviewed-by: Filip Filmar <fmil@google.com>
diff --git a/session_shells/gazelle/BUILD.gn b/session_shells/gazelle/BUILD.gn
index 5cb7a8f..f4e5186 100644
--- a/session_shells/gazelle/BUILD.gn
+++ b/session_shells/gazelle/BUILD.gn
@@ -15,6 +15,7 @@
testonly = true
deps = [
"appkit:tests",
+ "pointer_fusion:tests",
"wm:tests",
]
}
diff --git a/session_shells/gazelle/pointer_fusion/BUILD.gn b/session_shells/gazelle/pointer_fusion/BUILD.gn
new file mode 100644
index 0000000..09bcb62
--- /dev/null
+++ b/session_shells/gazelle/pointer_fusion/BUILD.gn
@@ -0,0 +1,35 @@
+# Copyright 2022 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import("//build/components.gni")
+import("//build/rust/rustc_library.gni")
+
+rustc_library("pointer_fusion") {
+ name = "pointer_fusion"
+ with_unit_tests = true
+ version = "0.1.0"
+ edition = "2018"
+ sources = [
+ "src/lib.rs",
+ "src/pointer/mod.rs",
+ "src/pointer/mouse.rs",
+ "src/pointer/touch.rs",
+ "src/tests.rs",
+ ]
+ deps = [
+ "//sdk/fidl/fuchsia.ui.pointer:fuchsia.ui.pointer_rust",
+ "//src/lib/zircon/rust:fuchsia-zircon",
+ "//third_party/rust_crates:futures",
+ "//third_party/rust_crates:num",
+ ]
+ test_deps = [ "//src/lib/fuchsia" ]
+}
+
+fuchsia_unittest_package("pointer_fusion_tests") {
+ deps = [ ":pointer_fusion_test" ]
+}
+
+group("tests") {
+ testonly = true
+ deps = [ ":pointer_fusion_tests" ]
+}
diff --git a/session_shells/gazelle/pointer_fusion/src/lib.rs b/session_shells/gazelle/pointer_fusion/src/lib.rs
new file mode 100644
index 0000000..6689e40
--- /dev/null
+++ b/session_shells/gazelle/pointer_fusion/src/lib.rs
@@ -0,0 +1,113 @@
+// Copyright 2022 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+mod pointer;
+
+#[cfg(test)]
+mod tests;
+
+use {
+ crate::pointer::PointerFusionState,
+ fidl_fuchsia_ui_pointer as fptr, fuchsia_zircon as zx,
+ futures::{
+ channel::mpsc::{self, UnboundedSender},
+ stream, Stream, StreamExt,
+ },
+};
+
+#[derive(Clone, Default, Debug)]
+pub enum DeviceKind {
+ #[default]
+ Touch,
+ Mouse,
+ Stylus,
+ InvertedStylus,
+ Trackpad,
+}
+
+#[derive(Clone, Default, Debug)]
+pub enum Phase {
+ #[default]
+ Cancel,
+ Add,
+ Remove,
+ Hover,
+ Down,
+ Move,
+ Up,
+}
+
+#[derive(Clone, Default, Debug)]
+pub enum SignalKind {
+ #[default]
+ None,
+ Scroll,
+ ScrollInertiaCancel,
+}
+
+pub const POINTER_BUTTON_1: i64 = 1 << 0;
+pub const POINTER_BUTTON_2: i64 = 1 << 1;
+pub const POINTER_BUTTON_3: i64 = 1 << 2;
+pub const POINTER_BUTTON_4: i64 = 1 << 3;
+pub const POINTER_BUTTON_5: i64 = 1 << 4;
+
+/// Information about the state of a pointer.
+#[derive(Clone, Default, Debug)]
+pub struct PointerEvent {
+ /// The monotonically increasing identifier that is present only on 'Down' events and
+ /// is 0 otherwise.
+ pub id: i64,
+ /// The kind of input device.
+ pub kind: DeviceKind,
+ /// The timestamp when the event originated. This is monotonically increasing for the same
+ /// [DeviceKind]. Timestamp for synthesized events is same as event synthesized from.
+ pub timestamp: zx::Time,
+ /// The current [Phase] of pointer event.
+ pub phase: Phase,
+ /// The unique device identifier.
+ pub device_id: u32,
+ /// The x position of the device, in the viewport's coordinate system, as reported by the raw
+ /// device event.
+ pub physical_x: f32,
+ /// The y position of the device, in the viewport's coordinate system, as reported by the raw
+ /// device event.
+ pub physical_y: f32,
+ /// The relative change in x position of the device from previous event in sequence.
+ pub physical_delta_x: f32,
+ /// The relative change in y position of the device from previous event in sequence.
+ pub physical_delta_y: f32,
+ /// The buttons pressed on the device represented as bitflags.
+ pub buttons: i64,
+ /// The event [SignalKind] for scroll events.
+ pub signal_kind: SignalKind,
+ /// The amount of scroll in x direction, in physical pixels.
+ pub scroll_delta_y: f64,
+ /// The amount of scroll in y direction, in physical pixels.
+ pub scroll_delta_x: f64,
+ /// Set if this [PointerEvent] was synthesized for maintaining legal input sequence.
+ pub synthesized: bool,
+}
+
+#[derive(Debug)]
+pub enum InputEvent {
+ MouseEvent(fptr::MouseEvent),
+ TouchEvent(fptr::TouchEvent),
+}
+
+/// Provides a stream of [PointerEvent] fused from [InputEvent::MouseEvent] and
+/// [InputEvent::TouchEvent].
+///
+/// * `pixel_ratio` - The device pixel ratio used to convert from logical to physical coordinates.
+///
+/// Returns a tuple of [UnboundedSender] to send [InputEvent]s to and a [Stream] to read fused
+/// [PointerEvent]s from.
+pub fn pointer_fusion(
+ pixel_ratio: f32,
+) -> (UnboundedSender<InputEvent>, impl Stream<Item = PointerEvent>) {
+ let mut state = PointerFusionState::new(pixel_ratio);
+ let (input_sender, receiver) = mpsc::unbounded::<InputEvent>();
+ let pointer_stream = receiver.map(move |input| stream::iter(state.fuse_input(input))).flatten();
+
+ (input_sender, pointer_stream)
+}
diff --git a/session_shells/gazelle/pointer_fusion/src/pointer/mod.rs b/session_shells/gazelle/pointer_fusion/src/pointer/mod.rs
new file mode 100644
index 0000000..5d90b91
--- /dev/null
+++ b/session_shells/gazelle/pointer_fusion/src/pointer/mod.rs
@@ -0,0 +1,76 @@
+// Copyright 2022 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+mod mouse;
+mod touch;
+
+use {
+ super::*,
+ fidl_fuchsia_ui_pointer as fptr, num,
+ std::collections::{HashMap, HashSet},
+ std::f32::EPSILON,
+};
+
+pub struct PointerFusionState {
+ pixel_ratio: f32,
+ mouse_device_info: HashMap<u32, fptr::MouseDeviceInfo>,
+ mouse_down: HashSet<u32>,
+ mouse_view_parameters: Option<fptr::ViewParameters>,
+ pointer_states: HashMap<u32, PointerState>,
+ next_pointer_id: i64,
+}
+
+impl PointerFusionState {
+ /// Constructs a [PointerFusionState] with a display [pixel_ratio] used to convert logical
+ /// coordinates into physical coordinates.
+ pub fn new(pixel_ratio: f32) -> Self {
+ PointerFusionState {
+ pixel_ratio,
+ mouse_device_info: HashMap::new(),
+ mouse_down: HashSet::new(),
+ mouse_view_parameters: None,
+ pointer_states: HashMap::new(),
+ next_pointer_id: 0,
+ }
+ }
+
+ pub fn fuse_input(&mut self, input: InputEvent) -> Vec<PointerEvent> {
+ match input {
+ InputEvent::MouseEvent(mouse_event) => self.fuse_mouse(mouse_event),
+ InputEvent::TouchEvent(touch_event) => self.fuse_touch(touch_event),
+ }
+ }
+}
+
+// The current information about a pointer derived from previous [PointerEvent]s. This is used to
+// sanitized the pointer stream and synthesize addition data like `physical_delta_x`.
+#[derive(Copy, Clone, Default)]
+struct PointerState {
+ id: i64,
+ is_down: bool,
+ physical_x: f32,
+ physical_y: f32,
+ buttons: i64,
+}
+
+impl PointerState {
+ pub(crate) fn from_event(event: &PointerEvent) -> Self {
+ let mut state = PointerState::default();
+ state.physical_x = event.physical_x;
+ state.physical_y = event.physical_y;
+ state
+ }
+
+ pub(crate) fn is_location_changed(&self, event: &PointerEvent) -> bool {
+ self.physical_x != event.physical_x || self.physical_y != event.physical_y
+ }
+
+ pub(crate) fn is_button_state_changed(&self, event: &PointerEvent) -> bool {
+ self.buttons != event.buttons
+ }
+
+ pub(crate) fn compute_delta(&self, event: &PointerEvent) -> (f32, f32) {
+ (event.physical_x - self.physical_x, event.physical_y - self.physical_y)
+ }
+}
diff --git a/session_shells/gazelle/pointer_fusion/src/pointer/mouse.rs b/session_shells/gazelle/pointer_fusion/src/pointer/mouse.rs
new file mode 100644
index 0000000..fed6308
--- /dev/null
+++ b/session_shells/gazelle/pointer_fusion/src/pointer/mouse.rs
@@ -0,0 +1,366 @@
+// Copyright 2022 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+use {
+ super::*, fidl_fuchsia_ui_pointer as fptr, fuchsia_zircon as zx, num, std::collections::HashSet,
+};
+
+const SCROLL_OFFSET_MULTIPLIER: i64 = 20;
+
+impl PointerFusionState {
+ // Converts raw [fptr::MouseEvent]s to one or more [PointerEvent]s.
+ pub(super) fn fuse_mouse(&mut self, event: fptr::MouseEvent) -> Vec<PointerEvent> {
+ if let Some(ref device_info) = event.device_info {
+ self.mouse_device_info.insert(device_info.id.unwrap_or(0), device_info.clone());
+ }
+
+ if event.view_parameters.is_some() {
+ self.mouse_view_parameters = event.view_parameters;
+ }
+
+ if has_valid_mouse_sample(&event) && self.mouse_view_parameters.is_some() {
+ let sample = event.pointer_sample.as_ref().unwrap();
+ let id = sample.device_id.unwrap();
+ if self.mouse_device_info.contains_key(&id) {
+ let any_button_down = sample.pressed_buttons.is_some();
+ let phase = compute_mouse_phase(any_button_down, &mut self.mouse_down, id);
+
+ let pointer_event = create_mouse_draft(
+ &event,
+ phase,
+ self.mouse_view_parameters.as_ref().unwrap(),
+ self.mouse_device_info.get(&id).unwrap(),
+ self.pixel_ratio,
+ );
+
+ let sanitized_events = self.sanitize_pointer(pointer_event);
+ return sanitized_events;
+ }
+ }
+
+ vec![]
+ }
+
+ // Sanitizes the [PointerEvent] draft such that the resulting event stream is contextually
+ // correct. It may drop events or synthesize new events to keep the event stream sane.
+ //
+ // Note: It is still possible to craft an event stream that will cause an assert check to fail
+ // on debug builds.
+ fn sanitize_pointer(&mut self, mut event: PointerEvent) -> Vec<PointerEvent> {
+ let mut converted_pointers = vec![];
+ match event.signal_kind {
+ SignalKind::None => match event.phase {
+ // Drops the Cancel if the pointer is not previously added.
+ Phase::Cancel => {
+ if let Some(state) = self.pointer_states.get_mut(&event.device_id) {
+ assert!(state.is_down);
+
+ event.id = state.id;
+ // Synthesize a move event if the location does not match.
+ if state.is_location_changed(&event) {
+ let (physical_delta_x, physical_delta_y) = state.compute_delta(&event);
+ let move_event = PointerEvent {
+ physical_delta_x,
+ physical_delta_y,
+ phase: Phase::Move,
+ synthesized: true,
+ ..event.clone()
+ };
+
+ state.physical_x = move_event.physical_x;
+ state.physical_y = move_event.physical_y;
+
+ converted_pointers.push(move_event);
+ }
+ state.is_down = false;
+ converted_pointers.push(event);
+ }
+ }
+ Phase::Add => {
+ assert!(!self.pointer_states.contains_key(&event.device_id));
+ let state = PointerState::from_event(&event);
+ self.pointer_states.insert(event.device_id, state);
+
+ converted_pointers.push(event);
+ }
+ Phase::Remove => {
+ assert!(self.pointer_states.contains_key(&event.device_id));
+ if let Some(state) = self.pointer_states.get_mut(&event.device_id) {
+ // Synthesize a Cancel event if pointer is down.
+ if state.is_down {
+ let mut cancel_event = event.clone();
+ cancel_event.phase = Phase::Cancel;
+ cancel_event.synthesized = true;
+ cancel_event.id = state.id;
+
+ state.is_down = false;
+ converted_pointers.push(cancel_event);
+ }
+
+ // Synthesize a hover event if the location does not match.
+ if state.is_location_changed(&event) {
+ let (physical_delta_x, physical_delta_y) = state.compute_delta(&event);
+ let hover_event = PointerEvent {
+ physical_delta_x,
+ physical_delta_y,
+ phase: Phase::Hover,
+ synthesized: true,
+ ..event.clone()
+ };
+
+ state.physical_x = hover_event.physical_x;
+ state.physical_y = hover_event.physical_y;
+
+ converted_pointers.push(hover_event);
+ }
+ }
+ self.pointer_states.remove(&event.device_id);
+ converted_pointers.push(event);
+ }
+ Phase::Hover => {
+ let mut state = match self.pointer_states.get_mut(&event.device_id) {
+ Some(state) => *state,
+ None => {
+ // Synthesize add event if the pointer is not previously added.
+ let mut add_event = event.clone();
+ add_event.phase = Phase::Add;
+ add_event.synthesized = true;
+ let state = PointerState::from_event(&add_event);
+ self.pointer_states.insert(add_event.device_id, state);
+
+ converted_pointers.push(add_event);
+ state
+ }
+ };
+
+ assert!(!state.is_down);
+ if state.is_location_changed(&event) {
+ let (physical_delta_x, physical_delta_y) = state.compute_delta(&event);
+ event.physical_delta_x = physical_delta_x;
+ event.physical_delta_y = physical_delta_y;
+
+ state.physical_x = event.physical_x;
+ state.physical_y = event.physical_y;
+ converted_pointers.push(event);
+ }
+ }
+ Phase::Down => {
+ let mut state = match self.pointer_states.get_mut(&event.device_id) {
+ Some(state) => *state,
+ None => {
+ // Synthesize add event if the pointer is not previously added.
+ let mut add_event = event.clone();
+ add_event.phase = Phase::Add;
+ add_event.synthesized = true;
+ let state = PointerState::from_event(&add_event);
+ self.pointer_states.insert(add_event.device_id, state);
+
+ converted_pointers.push(add_event);
+ state
+ }
+ };
+
+ assert!(!state.is_down);
+ // Synthesize a hover event if the location does not match.
+ if state.is_location_changed(&event) {
+ let (physical_delta_x, physical_delta_y) = state.compute_delta(&event);
+ let hover_event = PointerEvent {
+ physical_delta_x,
+ physical_delta_y,
+ phase: Phase::Hover,
+ synthesized: true,
+ ..event.clone()
+ };
+
+ state.physical_x = hover_event.physical_x;
+ state.physical_y = hover_event.physical_y;
+
+ converted_pointers.push(hover_event);
+ }
+ self.next_pointer_id += 1;
+ state.id = self.next_pointer_id;
+ state.is_down = true;
+ state.buttons = event.buttons;
+ self.pointer_states.insert(event.device_id, state);
+ converted_pointers.push(event);
+ }
+ Phase::Move => {
+ // Makes sure we have an existing pointer in down state
+ let mut state =
+ self.pointer_states.get_mut(&event.device_id).expect("State should exist");
+ assert!(state.is_down);
+ event.id = state.id;
+
+ // Skip this event if location does not change.
+ if state.is_location_changed(&event) || state.is_button_state_changed(&event) {
+ let (physical_delta_x, physical_delta_y) = state.compute_delta(&event);
+ event.physical_delta_x = physical_delta_x;
+ event.physical_delta_y = physical_delta_y;
+
+ state.physical_x = event.physical_x;
+ state.physical_y = event.physical_y;
+ state.buttons = event.buttons;
+ converted_pointers.push(event);
+ }
+ }
+ Phase::Up => {
+ // Makes sure we have an existing pointer in down state
+ let mut state =
+ self.pointer_states.get_mut(&event.device_id).expect("State should exist");
+ assert!(state.is_down);
+ event.id = state.id;
+
+ // Up phase should include which buttons where released.
+ let new_buttons = event.buttons;
+ event.buttons = state.buttons;
+
+ // Synthesize a move event if the location does not match.
+ if state.is_location_changed(&event) {
+ let (physical_delta_x, physical_delta_y) = state.compute_delta(&event);
+ let move_event = PointerEvent {
+ physical_delta_x,
+ physical_delta_y,
+ phase: Phase::Move,
+ synthesized: true,
+ ..event.clone()
+ };
+
+ state.physical_x = move_event.physical_x;
+ state.physical_y = move_event.physical_y;
+
+ converted_pointers.push(move_event);
+ }
+ state.is_down = false;
+ state.buttons = new_buttons;
+ converted_pointers.push(event);
+ }
+ },
+ // Handle scroll events.
+ _ => {}
+ }
+ converted_pointers
+ }
+}
+
+fn compute_mouse_phase(any_button_down: bool, mouse_down: &mut HashSet<u32>, id: u32) -> Phase {
+ if !mouse_down.contains(&id) && !any_button_down {
+ return Phase::Hover;
+ } else if !mouse_down.contains(&id) && any_button_down {
+ mouse_down.insert(id);
+ return Phase::Down;
+ } else if mouse_down.contains(&id) && any_button_down {
+ return Phase::Move;
+ } else if mouse_down.contains(&id) && !any_button_down {
+ mouse_down.remove(&id);
+ return Phase::Up;
+ } else {
+ return Phase::Cancel;
+ }
+}
+
+fn create_mouse_draft(
+ event: &fptr::MouseEvent,
+ phase: Phase,
+ view_parameters: &fptr::ViewParameters,
+ device_info: &fptr::MouseDeviceInfo,
+ pixel_ratio: f32,
+) -> PointerEvent {
+ assert!(has_valid_mouse_sample(event));
+
+ let sample = event.pointer_sample.as_ref().unwrap();
+
+ let mut pointer = PointerEvent::default();
+ pointer.timestamp = zx::Time::from_nanos(event.timestamp.unwrap_or(0));
+ pointer.phase = phase;
+ pointer.kind = DeviceKind::Mouse;
+ pointer.device_id = sample.device_id.unwrap_or(0);
+
+ let [logical_x, logical_y] =
+ viewport_to_view_coordinates(sample.position_in_viewport.unwrap(), view_parameters);
+ pointer.physical_x = logical_x * pixel_ratio;
+ pointer.physical_y = logical_y * pixel_ratio;
+
+ if sample.pressed_buttons.is_some() && device_info.buttons.is_some() {
+ let mut pointer_buttons: i64 = 0;
+ let pressed = sample.pressed_buttons.as_ref().unwrap();
+ let device_buttons = device_info.buttons.as_ref().unwrap();
+ for button_id in pressed {
+ if let Some(index) = device_buttons.iter().position(|&r| r == *button_id) {
+ pointer_buttons |= 1 << index;
+ }
+ }
+ pointer.buttons = pointer_buttons;
+ }
+
+ if sample.scroll_h.is_some()
+ || sample.scroll_v.is_some()
+ || sample.scroll_h_physical_pixel.is_some()
+ || sample.scroll_v_physical_pixel.is_some()
+ {
+ let tick_x_20ths = sample.scroll_h.unwrap_or(0) * SCROLL_OFFSET_MULTIPLIER;
+ let tick_y_20ths = sample.scroll_v.unwrap_or(0) * SCROLL_OFFSET_MULTIPLIER;
+ let offset_x = sample.scroll_h_physical_pixel.unwrap_or(tick_x_20ths as f64);
+ let offset_y = sample.scroll_v_physical_pixel.unwrap_or(tick_y_20ths as f64);
+
+ pointer.scroll_delta_x = offset_x;
+ pointer.scroll_delta_y = offset_y;
+ }
+
+ pointer
+}
+
+fn has_valid_mouse_sample(event: &fptr::MouseEvent) -> bool {
+ if event.pointer_sample.is_none() {
+ return false;
+ }
+ let sample = event.pointer_sample.as_ref().unwrap();
+ sample.device_id.is_some()
+ && sample.position_in_viewport.is_some()
+ && (sample.pressed_buttons.is_none()
+ || !sample.pressed_buttons.as_ref().unwrap().is_empty())
+}
+
+fn viewport_to_view_coordinates(
+ viewport_coordinates: [f32; 2],
+ view_parameters: &fptr::ViewParameters,
+) -> [f32; 2] {
+ let viewport_to_view_transform = view_parameters.viewport_to_view_transform;
+ // The transform matrix is a FIDL array with matrix data in column-major
+ // order. For a matrix with data [a b c d e f g h i], and with the viewport
+ // coordinates expressed as homogeneous coordinates, the logical view
+ // coordinates are obtained with the following formula:
+ // |a d g| |x| |x'|
+ // |b e h| * |y| = |y'|
+ // |c f i| |1| |w'|
+ // which we then normalize based on the w component:
+ // if z' not zero: (x'/w', y'/w')
+ // else (x', y')
+ let m = viewport_to_view_transform;
+ let x = viewport_coordinates[0];
+ let y = viewport_coordinates[1];
+ let xp = m[0] * x + m[3] * y + m[6];
+ let yp = m[1] * x + m[4] * y + m[7];
+ let wp = m[2] * x + m[5] * y + m[8];
+ let [x, y] = if wp > EPSILON { [xp / wp, yp / wp] } else { [xp, yp] };
+
+ clamp_to_view_space(x, y, view_parameters)
+}
+
+fn clamp_to_view_space(x: f32, y: f32, p: &fptr::ViewParameters) -> [f32; 2] {
+ let min_x = p.view.min[0];
+ let min_y = p.view.min[1];
+ let max_x = p.view.max[0];
+ let max_y = p.view.max[1];
+ if min_x <= x && x < max_x && min_y <= y && y < max_y {
+ return [x, y]; // No clamping to perform.
+ }
+
+ // View boundary is [min_x, max_x) x [min_y, max_y). Note that min is
+ // inclusive, but max is exclusive - so we subtract epsilon.
+ let max_x_inclusive = max_x - EPSILON;
+ let max_y_inclusive = max_y - EPSILON;
+ let clamped_x = num::clamp(x, min_x, max_x_inclusive);
+ let clamped_y = num::clamp(y, min_y, max_y_inclusive);
+ return [clamped_x, clamped_y];
+}
diff --git a/session_shells/gazelle/pointer_fusion/src/pointer/touch.rs b/session_shells/gazelle/pointer_fusion/src/pointer/touch.rs
new file mode 100644
index 0000000..4b35963
--- /dev/null
+++ b/session_shells/gazelle/pointer_fusion/src/pointer/touch.rs
@@ -0,0 +1,12 @@
+// Copyright 2022 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+use {super::*, fidl_fuchsia_ui_pointer as fptr};
+
+impl PointerFusionState {
+ pub(super) fn fuse_touch(&mut self, _event: fptr::TouchEvent) -> Vec<PointerEvent> {
+ // TODO(fxb/110099): Fuse touch events.
+ todo!("Fuse touch events");
+ }
+}
diff --git a/session_shells/gazelle/pointer_fusion/src/tests.rs b/session_shells/gazelle/pointer_fusion/src/tests.rs
new file mode 100644
index 0000000..5a3fa42
--- /dev/null
+++ b/session_shells/gazelle/pointer_fusion/src/tests.rs
@@ -0,0 +1,250 @@
+// Copyright 2022 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+use {
+ super::*,
+ fidl_fuchsia_ui_pointer as fptr,
+ // fidl_fuchsia_input_report as input_report,
+ futures::FutureExt,
+};
+
+#[fuchsia::test]
+async fn test_mouse_event_without_view_parameters() {
+ // Fusing mouse event without [fptr::ViewParameters] should not result in a PointerEvent.
+ let mouse_event = InputEvent::mouse();
+ let (sender, mut receiver) = pointer_fusion(1.0);
+ sender.unbounded_send(mouse_event).unwrap();
+ let pointer_event = receiver.next().now_or_never();
+ assert!(pointer_event.is_none(), "Received {:?}", pointer_event);
+}
+
+#[fuchsia::test]
+async fn test_mouse_event_without_mouse_sample() {
+ // Fusing mouse event without [fptr::MousePointerSample] should not result in a PointerEvent.
+ let mouse_event = InputEvent::mouse().view(1024.0, 600.0);
+ let (sender, mut receiver) = pointer_fusion(1.0);
+ sender.unbounded_send(mouse_event).unwrap();
+ let pointer_event = receiver.next().now_or_never();
+ assert!(pointer_event.is_none(), "Received {:?}", pointer_event);
+}
+
+#[fuchsia::test]
+async fn test_mouse_event_without_device_info() {
+ // Fusing mouse event without [fptr::MouseDeviceInfo] should not result in a PointerEvent.
+ let mouse_event = InputEvent::mouse().view(1024.0, 600.0).position(512.0, 300.0);
+ let (sender, mut receiver) = pointer_fusion(1.0);
+ sender.unbounded_send(mouse_event).unwrap();
+ let pointer_event = receiver.next().now_or_never();
+ assert!(pointer_event.is_none(), "Received {:?}", pointer_event);
+}
+
+#[fuchsia::test]
+async fn test_pixel_ratio() {
+ let mouse_event = InputEvent::mouse()
+ .view(1024.0, 600.0)
+ .device_info(42)
+ .position(512.0, 300.0)
+ .button_down();
+
+ let (sender, mut receiver) = pointer_fusion(2.0);
+ sender.unbounded_send(mouse_event).unwrap();
+
+ let pointer_event = receiver.next().await.unwrap();
+ assert!(matches!(pointer_event.phase, Phase::Add));
+ assert!(pointer_event.physical_x - 1024.0 < std::f32::EPSILON);
+ assert!(pointer_event.physical_y - 600.0 < std::f32::EPSILON);
+}
+
+#[fuchsia::test]
+async fn test_mouse_starts_with_add_event() {
+ let mouse_event =
+ InputEvent::mouse().view(1024.0, 600.0).device_info(42).position(512.0, 300.0);
+
+ let (sender, mut receiver) = pointer_fusion(1.0);
+ sender.unbounded_send(mouse_event).unwrap();
+ let pointer_event = receiver.next().await.unwrap();
+ assert!(matches!(pointer_event.phase, Phase::Add));
+}
+
+#[fuchsia::test]
+async fn test_mouse_tap() {
+ let mouse_event = InputEvent::mouse()
+ .view(1024.0, 600.0)
+ .device_info(42)
+ .position(512.0, 300.0)
+ .button_down();
+
+ let (sender, mut receiver) = pointer_fusion(1.0);
+ sender.unbounded_send(mouse_event).unwrap();
+
+ let pointer_event = receiver.next().await.unwrap();
+ assert!(matches!(pointer_event.phase, Phase::Add));
+
+ let pointer_event = receiver.next().await.unwrap();
+ assert!(matches!(pointer_event.phase, Phase::Down));
+
+ let mouse_event = InputEvent::mouse().device_info(42).position(512.0, 300.0);
+ sender.unbounded_send(mouse_event).unwrap();
+
+ let pointer_event = receiver.next().await.unwrap();
+ assert!(matches!(pointer_event.phase, Phase::Up));
+}
+
+#[fuchsia::test]
+async fn test_mouse_hover() {
+ let mouse_event =
+ InputEvent::mouse().view(1024.0, 600.0).device_info(42).position(512.0, 300.0);
+
+ let (sender, mut receiver) = pointer_fusion(1.0);
+ sender.unbounded_send(mouse_event).unwrap();
+
+ let pointer_event = receiver.next().await.unwrap();
+ assert!(matches!(pointer_event.phase, Phase::Add));
+
+ // Changing mouse x position should result in Hover event.
+ let mouse_event = InputEvent::mouse().device_info(42).position(540.0, 300.0);
+ sender.unbounded_send(mouse_event).unwrap();
+
+ let pointer_event = receiver.next().await.unwrap();
+ assert!(matches!(pointer_event.phase, Phase::Hover));
+ assert!(pointer_event.physical_x - 540.0 < std::f32::EPSILON);
+
+ // Changing mouse y position should result in Hover event.
+ let mouse_event = InputEvent::mouse().device_info(42).position(540.0, 320.0);
+ sender.unbounded_send(mouse_event).unwrap();
+
+ let pointer_event = receiver.next().await.unwrap();
+ assert!(matches!(pointer_event.phase, Phase::Hover));
+ assert!(pointer_event.physical_y - 320.0 < std::f32::EPSILON);
+}
+
+#[fuchsia::test]
+async fn test_mouse_move() {
+ let mouse_event = InputEvent::mouse()
+ .view(1024.0, 600.0)
+ .device_info(42)
+ .position(512.0, 300.0)
+ .button_down();
+
+ let (sender, mut receiver) = pointer_fusion(1.0);
+ sender.unbounded_send(mouse_event).unwrap();
+
+ let pointer_event = receiver.next().await.unwrap();
+ assert!(matches!(pointer_event.phase, Phase::Add));
+
+ let pointer_event = receiver.next().await.unwrap();
+ assert!(matches!(pointer_event.phase, Phase::Down));
+
+ // Changing the position should result in Move event.
+ let mouse_event = InputEvent::mouse().device_info(42).position(540.0, 320.0).button_down();
+ sender.unbounded_send(mouse_event).unwrap();
+
+ let pointer_event = receiver.next().await.unwrap();
+ assert!(matches!(pointer_event.phase, Phase::Move));
+ assert!(pointer_event.physical_delta_x == 28.0);
+ assert!(pointer_event.physical_delta_y == 20.0);
+
+ // Keeping the same position should not result in Move event.
+ let mouse_event = InputEvent::mouse().device_info(42).position(540.0, 320.0).button_down();
+ sender.unbounded_send(mouse_event).unwrap();
+
+ let pointer_event = receiver.next().now_or_never();
+ assert!(pointer_event.is_none(), "Received {:?}", pointer_event);
+}
+
+#[fuchsia::test]
+async fn test_mouse_no_spurious_hovers() {
+ let mouse_event =
+ InputEvent::mouse().view(1024.0, 600.0).device_info(42).position(512.0, 300.0);
+
+ let (sender, mut receiver) = pointer_fusion(1.0);
+ sender.unbounded_send(mouse_event).unwrap();
+
+ let pointer_event = receiver.next().await.unwrap();
+ assert!(matches!(pointer_event.phase, Phase::Add));
+
+ // Same position should not result in any hover event.
+ let mouse_event = InputEvent::mouse().device_info(42).position(512.0, 300.0);
+ sender.unbounded_send(mouse_event).unwrap();
+
+ let pointer_event = receiver.next().now_or_never();
+ assert!(pointer_event.is_none(), "Received {:?}", pointer_event);
+}
+
+trait TestMouseEvent {
+ fn mouse() -> Self;
+ fn view(self, width: f32, height: f32) -> Self;
+ fn device_info(self, id: u32) -> Self;
+ fn position(self, x: f32, y: f32) -> Self;
+ fn button_down(self) -> Self;
+}
+
+impl TestMouseEvent for InputEvent {
+ fn mouse() -> Self {
+ InputEvent::MouseEvent(fptr::MouseEvent { ..fptr::MouseEvent::EMPTY })
+ }
+
+ fn view(mut self, width: f32, height: f32) -> Self {
+ match self {
+ InputEvent::MouseEvent(ref mut event) => {
+ event.view_parameters = Some(fptr::ViewParameters {
+ view: fptr::Rectangle { min: [0.0, 0.0], max: [width, height] },
+ viewport: fptr::Rectangle { min: [0.0, 0.0], max: [width, height] },
+ viewport_to_view_transform: [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0],
+ });
+ }
+ _ => {}
+ }
+ self
+ }
+
+ fn device_info(mut self, id: u32) -> Self {
+ match self {
+ InputEvent::MouseEvent(ref mut event) => {
+ event.device_info = Some(fptr::MouseDeviceInfo {
+ id: Some(id),
+ buttons: Some([0, 1, 2].to_vec()),
+ relative_motion_range: None,
+ ..fptr::MouseDeviceInfo::EMPTY
+ });
+ }
+ _ => {}
+ }
+ self
+ }
+
+ fn position(mut self, x: f32, y: f32) -> Self {
+ match self {
+ InputEvent::MouseEvent(ref mut event) => {
+ let device_id = event
+ .device_info
+ .as_ref()
+ .unwrap_or(&fptr::MouseDeviceInfo {
+ id: Some(0),
+ ..fptr::MouseDeviceInfo::EMPTY
+ })
+ .id;
+ event.pointer_sample = Some(fptr::MousePointerSample {
+ device_id,
+ position_in_viewport: Some([x, y]),
+ ..fptr::MousePointerSample::EMPTY
+ });
+ }
+ _ => {}
+ }
+ self
+ }
+
+ fn button_down(mut self) -> Self {
+ match self {
+ InputEvent::MouseEvent(ref mut event) => {
+ if let Some(ref mut pointer_sample) = event.pointer_sample {
+ pointer_sample.pressed_buttons = Some(vec![0]);
+ }
+ }
+ _ => {}
+ }
+ self
+ }
+}