[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
+    }
+}