Add FlPointerManager to process pointer events from GTK in a form suitable for Flutter. (#56443)

This matches FlScrollingManager and FlKeyboardManager.

Add tests for this behaviour that was previously missing.
diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter
index d6da05f..4ee2e7f 100644
--- a/ci/licenses_golden/licenses_flutter
+++ b/ci/licenses_golden/licenses_flutter
@@ -45022,6 +45022,9 @@
 ORIGIN: ../../../flutter/shell/platform/linux/fl_plugin_registrar_private.h + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/shell/platform/linux/fl_plugin_registrar_test.cc + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/shell/platform/linux/fl_plugin_registry.cc + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/shell/platform/linux/fl_pointer_manager.cc + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/shell/platform/linux/fl_pointer_manager.h + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/shell/platform/linux/fl_pointer_manager_test.cc + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/shell/platform/linux/fl_renderable.cc + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/shell/platform/linux/fl_renderable.h + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/shell/platform/linux/fl_renderer.cc + ../../../flutter/LICENSE
@@ -47918,6 +47921,9 @@
 FILE: ../../../flutter/shell/platform/linux/fl_plugin_registrar_private.h
 FILE: ../../../flutter/shell/platform/linux/fl_plugin_registrar_test.cc
 FILE: ../../../flutter/shell/platform/linux/fl_plugin_registry.cc
+FILE: ../../../flutter/shell/platform/linux/fl_pointer_manager.cc
+FILE: ../../../flutter/shell/platform/linux/fl_pointer_manager.h
+FILE: ../../../flutter/shell/platform/linux/fl_pointer_manager_test.cc
 FILE: ../../../flutter/shell/platform/linux/fl_renderable.cc
 FILE: ../../../flutter/shell/platform/linux/fl_renderable.h
 FILE: ../../../flutter/shell/platform/linux/fl_renderer.cc
diff --git a/shell/platform/linux/BUILD.gn b/shell/platform/linux/BUILD.gn
index d018c0c..b11f734 100644
--- a/shell/platform/linux/BUILD.gn
+++ b/shell/platform/linux/BUILD.gn
@@ -92,6 +92,7 @@
              "fl_method_channel_private.h",
              "fl_method_codec_private.h",
              "fl_plugin_registrar_private.h",
+             "fl_pointer_manager.h",
              "fl_window_state_monitor.h",
              "key_mapping.h",
            ]
@@ -130,6 +131,7 @@
     "fl_platform_handler.cc",
     "fl_plugin_registrar.cc",
     "fl_plugin_registry.cc",
+    "fl_pointer_manager.cc",
     "fl_renderable.cc",
     "fl_renderer.cc",
     "fl_renderer_gdk.cc",
@@ -224,6 +226,7 @@
     "fl_pixel_buffer_texture_test.cc",
     "fl_platform_handler_test.cc",
     "fl_plugin_registrar_test.cc",
+    "fl_pointer_manager_test.cc",
     "fl_renderer_test.cc",
     "fl_scrolling_manager_test.cc",
     "fl_settings_handler_test.cc",
diff --git a/shell/platform/linux/fl_pointer_manager.cc b/shell/platform/linux/fl_pointer_manager.cc
new file mode 100644
index 0000000..67f360b
--- /dev/null
+++ b/shell/platform/linux/fl_pointer_manager.cc
@@ -0,0 +1,203 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "flutter/shell/platform/linux/fl_pointer_manager.h"
+
+#include "flutter/shell/platform/linux/fl_engine_private.h"
+
+static constexpr int kMicrosecondsPerMillisecond = 1000;
+
+struct _FlPointerManager {
+  GObject parent_instance;
+
+  // Engine to send pointer events to.
+  GWeakRef engine;
+
+  // ID to mark events with.
+  FlutterViewId view_id;
+
+  // TRUE if the mouse pointer is inside the view, used for generating missing
+  // add events.
+  gboolean pointer_inside;
+
+  // Pointer button state recorded for sending status updates.
+  int64_t button_state;
+};
+
+G_DEFINE_TYPE(FlPointerManager, fl_pointer_manager, G_TYPE_OBJECT);
+
+// Generates a mouse pointer event if the pointer appears inside the window.
+static void ensure_pointer_added(FlPointerManager* self,
+                                 guint event_time,
+                                 FlutterPointerDeviceKind device_kind,
+                                 gdouble x,
+                                 gdouble y) {
+  if (self->pointer_inside) {
+    return;
+  }
+  self->pointer_inside = TRUE;
+
+  g_autoptr(FlEngine) engine = FL_ENGINE(g_weak_ref_get(&self->engine));
+  if (engine == nullptr) {
+    return;
+  }
+
+  fl_engine_send_mouse_pointer_event(
+      engine, self->view_id, kAdd, event_time * kMicrosecondsPerMillisecond, x,
+      y, device_kind, 0, 0, self->button_state);
+}
+
+static void fl_pointer_manager_dispose(GObject* object) {
+  FlPointerManager* self = FL_POINTER_MANAGER(object);
+
+  g_weak_ref_clear(&self->engine);
+
+  G_OBJECT_CLASS(fl_pointer_manager_parent_class)->dispose(object);
+}
+
+static void fl_pointer_manager_class_init(FlPointerManagerClass* klass) {
+  G_OBJECT_CLASS(klass)->dispose = fl_pointer_manager_dispose;
+}
+
+static void fl_pointer_manager_init(FlPointerManager* self) {}
+
+FlPointerManager* fl_pointer_manager_new(FlutterViewId view_id,
+                                         FlEngine* engine) {
+  FlPointerManager* self =
+      FL_POINTER_MANAGER(g_object_new(fl_pointer_manager_get_type(), nullptr));
+
+  self->view_id = view_id;
+  g_weak_ref_init(&self->engine, engine);
+
+  return self;
+}
+
+gboolean fl_pointer_manager_handle_button_press(
+    FlPointerManager* self,
+    guint event_time,
+    FlutterPointerDeviceKind device_kind,
+    gdouble x,
+    gdouble y,
+    int64_t button) {
+  g_return_val_if_fail(FL_IS_POINTER_MANAGER(self), FALSE);
+
+  ensure_pointer_added(self, event_time, device_kind, x, y);
+
+  // Drop the event if Flutter already thinks the button is down.
+  if ((self->button_state & button) != 0) {
+    return FALSE;
+  }
+
+  int old_button_state = self->button_state;
+  FlutterPointerPhase phase = kMove;
+  self->button_state ^= button;
+  phase = old_button_state == 0 ? kDown : kMove;
+
+  g_autoptr(FlEngine) engine = FL_ENGINE(g_weak_ref_get(&self->engine));
+  if (engine == nullptr) {
+    return FALSE;
+  }
+
+  fl_engine_send_mouse_pointer_event(
+      engine, self->view_id, phase, event_time * kMicrosecondsPerMillisecond, x,
+      y, device_kind, 0, 0, self->button_state);
+
+  return TRUE;
+}
+
+gboolean fl_pointer_manager_handle_button_release(
+    FlPointerManager* self,
+    guint event_time,
+    FlutterPointerDeviceKind device_kind,
+    gdouble x,
+    gdouble y,
+    int64_t button) {
+  g_return_val_if_fail(FL_IS_POINTER_MANAGER(self), FALSE);
+
+  // Drop the event if Flutter already thinks the button is up.
+  if ((self->button_state & button) == 0) {
+    return FALSE;
+  }
+
+  FlutterPointerPhase phase = kMove;
+  self->button_state ^= button;
+
+  phase = self->button_state == 0 ? kUp : kMove;
+
+  g_autoptr(FlEngine) engine = FL_ENGINE(g_weak_ref_get(&self->engine));
+  if (engine == nullptr) {
+    return FALSE;
+  }
+
+  fl_engine_send_mouse_pointer_event(
+      engine, self->view_id, phase, event_time * kMicrosecondsPerMillisecond, x,
+      y, device_kind, 0, 0, self->button_state);
+
+  return TRUE;
+}
+
+gboolean fl_pointer_manager_handle_motion(FlPointerManager* self,
+                                          guint event_time,
+                                          FlutterPointerDeviceKind device_kind,
+                                          gdouble x,
+                                          gdouble y) {
+  g_return_val_if_fail(FL_IS_POINTER_MANAGER(self), FALSE);
+
+  g_autoptr(FlEngine) engine = FL_ENGINE(g_weak_ref_get(&self->engine));
+  if (engine == nullptr) {
+    return FALSE;
+  }
+
+  ensure_pointer_added(self, event_time, device_kind, x, y);
+
+  fl_engine_send_mouse_pointer_event(
+      engine, self->view_id, self->button_state != 0 ? kMove : kHover,
+      event_time * kMicrosecondsPerMillisecond, x, y, device_kind, 0, 0,
+      self->button_state);
+
+  return TRUE;
+}
+
+gboolean fl_pointer_manager_handle_enter(FlPointerManager* self,
+                                         guint event_time,
+                                         FlutterPointerDeviceKind device_kind,
+                                         gdouble x,
+                                         gdouble y) {
+  g_return_val_if_fail(FL_IS_POINTER_MANAGER(self), FALSE);
+
+  g_autoptr(FlEngine) engine = FL_ENGINE(g_weak_ref_get(&self->engine));
+  if (engine == nullptr) {
+    return FALSE;
+  }
+
+  ensure_pointer_added(self, event_time, device_kind, x, y);
+
+  return TRUE;
+}
+
+gboolean fl_pointer_manager_handle_leave(FlPointerManager* self,
+                                         guint event_time,
+                                         FlutterPointerDeviceKind device_kind,
+                                         gdouble x,
+                                         gdouble y) {
+  g_return_val_if_fail(FL_IS_POINTER_MANAGER(self), FALSE);
+
+  g_autoptr(FlEngine) engine = FL_ENGINE(g_weak_ref_get(&self->engine));
+  if (engine == nullptr) {
+    return FALSE;
+  }
+
+  // Don't remove pointer while button is down; In case of dragging outside of
+  // window with mouse grab active Gtk will send another leave notify on
+  // release.
+  if (self->pointer_inside && self->button_state == 0) {
+    fl_engine_send_mouse_pointer_event(engine, self->view_id, kRemove,
+                                       event_time * kMicrosecondsPerMillisecond,
+                                       x, y, device_kind, 0, 0,
+                                       self->button_state);
+    self->pointer_inside = FALSE;
+  }
+
+  return TRUE;
+}
diff --git a/shell/platform/linux/fl_pointer_manager.h b/shell/platform/linux/fl_pointer_manager.h
new file mode 100644
index 0000000..2647512
--- /dev/null
+++ b/shell/platform/linux/fl_pointer_manager.h
@@ -0,0 +1,118 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef FLUTTER_SHELL_PLATFORM_LINUX_FL_POINTER_MANAGER_H_
+#define FLUTTER_SHELL_PLATFORM_LINUX_FL_POINTER_MANAGER_H_
+
+#include "flutter/shell/platform/embedder/embedder.h"
+#include "flutter/shell/platform/linux/public/flutter_linux/fl_engine.h"
+
+G_BEGIN_DECLS
+
+G_DECLARE_FINAL_TYPE(FlPointerManager,
+                     fl_pointer_manager,
+                     FL,
+                     POINTER_MANAGER,
+                     GObject);
+
+/**
+ * fl_pointer_manager_new:
+ * @view_id: view ID to report events for.
+ * @engine: an #FlEngine.
+ *
+ * Create a new #FlPointerManager.
+ *
+ * Returns: a new #FlPointerManager.
+ */
+FlPointerManager* fl_pointer_manager_new(FlutterViewId view_id,
+                                         FlEngine* engine);
+
+/**
+ * fl_pointer_manager_handle_button_press:
+ * @manager: an #FlPointerManager.
+ * @event_time: event time in milliseconds.
+ * @device_kind: kind of device generating the event.
+ * @x: x co-ordinate of event.
+ * @y: y co-ordinate of event.
+ * @button: button being pressed.
+ *
+ * Returns %TRUE if this event was handled.
+ */
+gboolean fl_pointer_manager_handle_button_press(
+    FlPointerManager* manager,
+    guint event_time,
+    FlutterPointerDeviceKind device_kind,
+    gdouble x,
+    gdouble y,
+    int64_t button);
+
+/**
+ * fl_pointer_manager_handle_button_release:
+ * @manager: an #FlPointerManager.
+ * @event_time: event time in milliseconds.
+ * @device_kind: kind of device generating the event.
+ * @x: x co-ordinate of event.
+ * @y: y co-ordinate of event.
+ * @button: button being released.
+ *
+ * Returns %TRUE if this event was handled.
+ */
+gboolean fl_pointer_manager_handle_button_release(
+    FlPointerManager* manager,
+    guint event_time,
+    FlutterPointerDeviceKind device_kind,
+    gdouble x,
+    gdouble y,
+    int64_t button);
+
+/**
+ * fl_pointer_manager_handle_motion:
+ * @manager: an #FlPointerManager.
+ * @event_time: event time in milliseconds.
+ * @device_kind: kind of device generating the event.
+ * @x: x co-ordinate of event.
+ * @y: y co-ordinate of event.
+ *
+ * Returns %TRUE if this event was handled.
+ */
+gboolean fl_pointer_manager_handle_motion(FlPointerManager* manager,
+                                          guint event_time,
+                                          FlutterPointerDeviceKind device_kind,
+                                          gdouble x,
+                                          gdouble y);
+
+/**
+ * fl_pointer_manager_handle_enter:
+ * @manager: an #FlPointerManager.
+ * @event_time: event time in milliseconds.
+ * @device_kind: kind of device generating the event.
+ * @x: x co-ordinate of event.
+ * @y: y co-ordinate of event.
+ *
+ * Returns %TRUE if this event was handled.
+ */
+gboolean fl_pointer_manager_handle_enter(FlPointerManager* manager,
+                                         guint event_time,
+                                         FlutterPointerDeviceKind device_kind,
+                                         gdouble x,
+                                         gdouble y);
+
+/**
+ * fl_pointer_manager_handle_leave:
+ * @manager: an #FlPointerManager.
+ * @event_time: event time in milliseconds.
+ * @device_kind: kind of device generating the event.
+ * @x: x co-ordinate of event.
+ * @y: y co-ordinate of event.
+ *
+ * Returns %TRUE if this event was handled.
+ */
+gboolean fl_pointer_manager_handle_leave(FlPointerManager* manager,
+                                         guint event_time,
+                                         FlutterPointerDeviceKind device_kind,
+                                         gdouble x,
+                                         gdouble y);
+G_END_DECLS
+
+#endif  // FLUTTER_SHELL_PLATFORM_LINUX_FL_POINTER_MANAGER_H_
diff --git a/shell/platform/linux/fl_pointer_manager_test.cc b/shell/platform/linux/fl_pointer_manager_test.cc
new file mode 100644
index 0000000..503397e
--- /dev/null
+++ b/shell/platform/linux/fl_pointer_manager_test.cc
@@ -0,0 +1,448 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "flutter/shell/platform/linux/fl_pointer_manager.h"
+#include "flutter/shell/platform/embedder/test_utils/proc_table_replacement.h"
+#include "flutter/shell/platform/linux/fl_engine_private.h"
+#include "flutter/shell/platform/linux/testing/fl_test.h"
+
+#include "gtest/gtest.h"
+
+static void log_pointer_events(
+    FlEngine* engine,
+    std::vector<FlutterPointerEvent>& pointer_events) {
+  FlutterEngineProcTable* embedder_api = fl_engine_get_embedder_api(engine);
+  embedder_api->SendPointerEvent = MOCK_ENGINE_PROC(
+      SendPointerEvent,
+      ([&pointer_events](auto engine, const FlutterPointerEvent* events,
+                         size_t events_count) {
+        for (size_t i = 0; i < events_count; i++) {
+          pointer_events.push_back(events[i]);
+        }
+
+        return kSuccess;
+      }));
+}
+
+TEST(FlPointerManagerTest, EnterLeave) {
+  g_autoptr(FlEngine) engine = make_mock_engine();
+  std::vector<FlutterPointerEvent> pointer_events;
+  log_pointer_events(engine, pointer_events);
+
+  g_autoptr(FlPointerManager) manager = fl_pointer_manager_new(42, engine);
+  fl_pointer_manager_handle_enter(manager, 1234, kFlutterPointerDeviceKindMouse,
+                                  1.0, 2.0);
+  fl_pointer_manager_handle_leave(manager, 1235, kFlutterPointerDeviceKindMouse,
+                                  3.0, 4.0);
+
+  EXPECT_EQ(pointer_events.size(), 2u);
+
+  EXPECT_EQ(pointer_events[0].timestamp, 1234000u);
+  EXPECT_EQ(pointer_events[0].x, 1.0);
+  EXPECT_EQ(pointer_events[0].y, 2.0);
+  EXPECT_EQ(pointer_events[0].device_kind, kFlutterPointerDeviceKindMouse);
+  EXPECT_EQ(pointer_events[0].buttons, 0);
+  EXPECT_EQ(pointer_events[0].view_id, 42);
+
+  EXPECT_EQ(pointer_events[1].timestamp, 1235000u);
+  EXPECT_EQ(pointer_events[1].x, 3.0);
+  EXPECT_EQ(pointer_events[1].y, 4.0);
+  EXPECT_EQ(pointer_events[1].device_kind, kFlutterPointerDeviceKindMouse);
+  EXPECT_EQ(pointer_events[1].buttons, 0);
+  EXPECT_EQ(pointer_events[1].view_id, 42);
+}
+
+TEST(FlPointerManagerTest, EnterEnter) {
+  g_autoptr(FlEngine) engine = make_mock_engine();
+  std::vector<FlutterPointerEvent> pointer_events;
+  log_pointer_events(engine, pointer_events);
+
+  g_autoptr(FlPointerManager) manager = fl_pointer_manager_new(42, engine);
+  fl_pointer_manager_handle_enter(manager, 1234, kFlutterPointerDeviceKindMouse,
+                                  1.0, 2.0);
+  // Duplicate enter is ignored
+  fl_pointer_manager_handle_enter(manager, 1235, kFlutterPointerDeviceKindMouse,
+                                  3.0, 4.0);
+
+  EXPECT_EQ(pointer_events.size(), 1u);
+
+  EXPECT_EQ(pointer_events[0].timestamp, 1234000u);
+  EXPECT_EQ(pointer_events[0].x, 1.0);
+  EXPECT_EQ(pointer_events[0].y, 2.0);
+  EXPECT_EQ(pointer_events[0].device_kind, kFlutterPointerDeviceKindMouse);
+  EXPECT_EQ(pointer_events[0].buttons, 0);
+  EXPECT_EQ(pointer_events[0].view_id, 42);
+}
+
+TEST(FlPointerManagerTest, EnterLeaveLeave) {
+  g_autoptr(FlEngine) engine = make_mock_engine();
+  std::vector<FlutterPointerEvent> pointer_events;
+  log_pointer_events(engine, pointer_events);
+
+  g_autoptr(FlPointerManager) manager = fl_pointer_manager_new(42, engine);
+  fl_pointer_manager_handle_enter(manager, 1234, kFlutterPointerDeviceKindMouse,
+                                  1.0, 2.0);
+  fl_pointer_manager_handle_leave(manager, 1235, kFlutterPointerDeviceKindMouse,
+                                  3.0, 4.0);
+  // Duplicate leave is ignored
+  fl_pointer_manager_handle_leave(manager, 1235, kFlutterPointerDeviceKindMouse,
+                                  5.0, 6.0);
+
+  EXPECT_EQ(pointer_events.size(), 2u);
+
+  EXPECT_EQ(pointer_events[0].timestamp, 1234000u);
+  EXPECT_EQ(pointer_events[0].x, 1.0);
+  EXPECT_EQ(pointer_events[0].y, 2.0);
+  EXPECT_EQ(pointer_events[0].device_kind, kFlutterPointerDeviceKindMouse);
+  EXPECT_EQ(pointer_events[0].buttons, 0);
+  EXPECT_EQ(pointer_events[0].view_id, 42);
+
+  EXPECT_EQ(pointer_events[1].timestamp, 1235000u);
+  EXPECT_EQ(pointer_events[1].x, 3.0);
+  EXPECT_EQ(pointer_events[1].y, 4.0);
+  EXPECT_EQ(pointer_events[1].device_kind, kFlutterPointerDeviceKindMouse);
+  EXPECT_EQ(pointer_events[1].buttons, 0);
+  EXPECT_EQ(pointer_events[1].view_id, 42);
+}
+
+TEST(FlPointerManagerTest, EnterButtonPress) {
+  g_autoptr(FlEngine) engine = make_mock_engine();
+  std::vector<FlutterPointerEvent> pointer_events;
+  log_pointer_events(engine, pointer_events);
+
+  g_autoptr(FlPointerManager) manager = fl_pointer_manager_new(42, engine);
+  fl_pointer_manager_handle_enter(manager, 1234, kFlutterPointerDeviceKindMouse,
+                                  1.0, 2.0);
+  fl_pointer_manager_handle_button_press(
+      manager, 1235, kFlutterPointerDeviceKindMouse, 4.0, 8.0,
+      kFlutterPointerButtonMousePrimary);
+
+  EXPECT_EQ(pointer_events.size(), 2u);
+
+  EXPECT_EQ(pointer_events[0].timestamp, 1234000u);
+  EXPECT_EQ(pointer_events[0].x, 1.0);
+  EXPECT_EQ(pointer_events[0].y, 2.0);
+  EXPECT_EQ(pointer_events[0].device_kind, kFlutterPointerDeviceKindMouse);
+  EXPECT_EQ(pointer_events[0].buttons, 0);
+  EXPECT_EQ(pointer_events[0].view_id, 42);
+
+  EXPECT_EQ(pointer_events[1].timestamp, 1235000u);
+  EXPECT_EQ(pointer_events[1].x, 4.0);
+  EXPECT_EQ(pointer_events[1].y, 8.0);
+  EXPECT_EQ(pointer_events[1].device_kind, kFlutterPointerDeviceKindMouse);
+  EXPECT_EQ(pointer_events[1].buttons, kFlutterPointerButtonMousePrimary);
+  EXPECT_EQ(pointer_events[1].view_id, 42);
+}
+
+TEST(FlPointerManagerTest, NoEnterButtonPress) {
+  g_autoptr(FlEngine) engine = make_mock_engine();
+  std::vector<FlutterPointerEvent> pointer_events;
+  log_pointer_events(engine, pointer_events);
+
+  g_autoptr(FlPointerManager) manager = fl_pointer_manager_new(42, engine);
+  fl_pointer_manager_handle_button_press(
+      manager, 1234, kFlutterPointerDeviceKindMouse, 4.0, 8.0,
+      kFlutterPointerButtonMousePrimary);
+
+  EXPECT_EQ(pointer_events.size(), 2u);
+
+  // Synthetic enter events
+  EXPECT_EQ(pointer_events[0].timestamp, 1234000u);
+  EXPECT_EQ(pointer_events[0].x, 4.0);
+  EXPECT_EQ(pointer_events[0].y, 8.0);
+  EXPECT_EQ(pointer_events[0].device_kind, kFlutterPointerDeviceKindMouse);
+  EXPECT_EQ(pointer_events[0].buttons, 0);
+  EXPECT_EQ(pointer_events[0].view_id, 42);
+
+  EXPECT_EQ(pointer_events[1].timestamp, 1234000u);
+  EXPECT_EQ(pointer_events[1].x, 4.0);
+  EXPECT_EQ(pointer_events[1].y, 8.0);
+  EXPECT_EQ(pointer_events[1].device_kind, kFlutterPointerDeviceKindMouse);
+  EXPECT_EQ(pointer_events[1].buttons, kFlutterPointerButtonMousePrimary);
+  EXPECT_EQ(pointer_events[1].view_id, 42);
+}
+
+TEST(FlPointerManagerTest, ButtonPressButtonRelease) {
+  g_autoptr(FlEngine) engine = make_mock_engine();
+  std::vector<FlutterPointerEvent> pointer_events;
+  log_pointer_events(engine, pointer_events);
+
+  g_autoptr(FlPointerManager) manager = fl_pointer_manager_new(42, engine);
+  fl_pointer_manager_handle_button_press(
+      manager, 1234, kFlutterPointerDeviceKindMouse, 4.0, 8.0,
+      kFlutterPointerButtonMousePrimary);
+  fl_pointer_manager_handle_button_release(
+      manager, 1235, kFlutterPointerDeviceKindMouse, 5.0, 9.0,
+      kFlutterPointerButtonMousePrimary);
+
+  EXPECT_EQ(pointer_events.size(), 3u);
+
+  // Ignore first synthetic enter event
+  EXPECT_EQ(pointer_events[1].timestamp, 1234000u);
+  EXPECT_EQ(pointer_events[1].x, 4.0);
+  EXPECT_EQ(pointer_events[1].y, 8.0);
+  EXPECT_EQ(pointer_events[1].device_kind, kFlutterPointerDeviceKindMouse);
+  EXPECT_EQ(pointer_events[1].buttons, kFlutterPointerButtonMousePrimary);
+  EXPECT_EQ(pointer_events[1].view_id, 42);
+  EXPECT_EQ(pointer_events[2].timestamp, 1235000u);
+  EXPECT_EQ(pointer_events[2].x, 5.0);
+  EXPECT_EQ(pointer_events[2].y, 9.0);
+  EXPECT_EQ(pointer_events[2].device_kind, kFlutterPointerDeviceKindMouse);
+  EXPECT_EQ(pointer_events[2].buttons, 0);
+  EXPECT_EQ(pointer_events[2].view_id, 42);
+}
+
+TEST(FlPointerManagerTest, ButtonPressButtonReleaseThreeButtons) {
+  g_autoptr(FlEngine) engine = make_mock_engine();
+  std::vector<FlutterPointerEvent> pointer_events;
+  log_pointer_events(engine, pointer_events);
+
+  g_autoptr(FlPointerManager) manager = fl_pointer_manager_new(42, engine);
+  // Press buttons 1-2-3, release 3-2-1
+  fl_pointer_manager_handle_button_press(
+      manager, 1234, kFlutterPointerDeviceKindMouse, 1.0, 2.0,
+      kFlutterPointerButtonMousePrimary);
+  fl_pointer_manager_handle_button_press(
+      manager, 1235, kFlutterPointerDeviceKindMouse, 3.0, 4.0,
+      kFlutterPointerButtonMouseSecondary);
+  fl_pointer_manager_handle_button_press(manager, 1236,
+                                         kFlutterPointerDeviceKindMouse, 5.0,
+                                         6.0, kFlutterPointerButtonMouseMiddle);
+  fl_pointer_manager_handle_button_release(
+      manager, 1237, kFlutterPointerDeviceKindMouse, 7.0, 8.0,
+      kFlutterPointerButtonMouseMiddle);
+  fl_pointer_manager_handle_button_release(
+      manager, 1238, kFlutterPointerDeviceKindMouse, 9.0, 10.0,
+      kFlutterPointerButtonMouseSecondary);
+  fl_pointer_manager_handle_button_release(
+      manager, 1239, kFlutterPointerDeviceKindMouse, 11.0, 12.0,
+      kFlutterPointerButtonMousePrimary);
+
+  EXPECT_EQ(pointer_events.size(), 7u);
+
+  // Ignore first synthetic enter event
+  EXPECT_EQ(pointer_events[1].timestamp, 1234000u);
+  EXPECT_EQ(pointer_events[1].x, 1.0);
+  EXPECT_EQ(pointer_events[1].y, 2.0);
+  EXPECT_EQ(pointer_events[1].buttons, kFlutterPointerButtonMousePrimary);
+  EXPECT_EQ(pointer_events[2].timestamp, 1235000u);
+  EXPECT_EQ(pointer_events[2].x, 3.0);
+  EXPECT_EQ(pointer_events[2].y, 4.0);
+  EXPECT_EQ(pointer_events[2].buttons, kFlutterPointerButtonMousePrimary |
+                                           kFlutterPointerButtonMouseSecondary);
+  EXPECT_EQ(pointer_events[3].timestamp, 1236000u);
+  EXPECT_EQ(pointer_events[3].x, 5.0);
+  EXPECT_EQ(pointer_events[3].y, 6.0);
+  EXPECT_EQ(pointer_events[3].buttons, kFlutterPointerButtonMousePrimary |
+                                           kFlutterPointerButtonMouseSecondary |
+                                           kFlutterPointerButtonMouseMiddle);
+  EXPECT_EQ(pointer_events[4].timestamp, 1237000u);
+  EXPECT_EQ(pointer_events[4].x, 7.0);
+  EXPECT_EQ(pointer_events[4].y, 8.0);
+  EXPECT_EQ(pointer_events[4].buttons, kFlutterPointerButtonMousePrimary |
+                                           kFlutterPointerButtonMouseSecondary);
+  EXPECT_EQ(pointer_events[5].timestamp, 1238000u);
+  EXPECT_EQ(pointer_events[5].x, 9.0);
+  EXPECT_EQ(pointer_events[5].y, 10.0);
+  EXPECT_EQ(pointer_events[5].buttons, kFlutterPointerButtonMousePrimary);
+  EXPECT_EQ(pointer_events[6].timestamp, 1239000u);
+  EXPECT_EQ(pointer_events[6].x, 11.0);
+  EXPECT_EQ(pointer_events[6].y, 12.0);
+  EXPECT_EQ(pointer_events[6].buttons, 0);
+}
+
+TEST(FlPointerManagerTest, ButtonPressButtonPressButtonRelease) {
+  g_autoptr(FlEngine) engine = make_mock_engine();
+  std::vector<FlutterPointerEvent> pointer_events;
+  log_pointer_events(engine, pointer_events);
+
+  g_autoptr(FlPointerManager) manager = fl_pointer_manager_new(42, engine);
+  fl_pointer_manager_handle_button_press(
+      manager, 1234, kFlutterPointerDeviceKindMouse, 4.0, 8.0,
+      kFlutterPointerButtonMousePrimary);
+  // Ignore duplicate press
+  fl_pointer_manager_handle_button_press(
+      manager, 1234, kFlutterPointerDeviceKindMouse, 6.0, 10.0,
+      kFlutterPointerButtonMousePrimary);
+  fl_pointer_manager_handle_button_release(
+      manager, 1235, kFlutterPointerDeviceKindMouse, 5.0, 9.0,
+      kFlutterPointerButtonMousePrimary);
+
+  EXPECT_EQ(pointer_events.size(), 3u);
+
+  // Ignore first synthetic enter event
+  EXPECT_EQ(pointer_events[1].timestamp, 1234000u);
+  EXPECT_EQ(pointer_events[1].x, 4.0);
+  EXPECT_EQ(pointer_events[1].y, 8.0);
+  EXPECT_EQ(pointer_events[1].device_kind, kFlutterPointerDeviceKindMouse);
+  EXPECT_EQ(pointer_events[1].buttons, kFlutterPointerButtonMousePrimary);
+  EXPECT_EQ(pointer_events[1].view_id, 42);
+  EXPECT_EQ(pointer_events[2].timestamp, 1235000u);
+  EXPECT_EQ(pointer_events[2].x, 5.0);
+  EXPECT_EQ(pointer_events[2].y, 9.0);
+  EXPECT_EQ(pointer_events[2].device_kind, kFlutterPointerDeviceKindMouse);
+  EXPECT_EQ(pointer_events[2].buttons, 0);
+  EXPECT_EQ(pointer_events[2].view_id, 42);
+}
+
+TEST(FlPointerManagerTest, ButtonPressButtonReleaseButtonRelease) {
+  g_autoptr(FlEngine) engine = make_mock_engine();
+  std::vector<FlutterPointerEvent> pointer_events;
+  log_pointer_events(engine, pointer_events);
+
+  g_autoptr(FlPointerManager) manager = fl_pointer_manager_new(42, engine);
+  fl_pointer_manager_handle_button_press(
+      manager, 1234, kFlutterPointerDeviceKindMouse, 4.0, 8.0,
+      kFlutterPointerButtonMousePrimary);
+  fl_pointer_manager_handle_button_release(
+      manager, 1235, kFlutterPointerDeviceKindMouse, 5.0, 9.0,
+      kFlutterPointerButtonMousePrimary);
+  // Ignore duplicate release
+  fl_pointer_manager_handle_button_release(
+      manager, 1235, kFlutterPointerDeviceKindMouse, 6.0, 10.0,
+      kFlutterPointerButtonMousePrimary);
+
+  EXPECT_EQ(pointer_events.size(), 3u);
+
+  // Ignore first synthetic enter event
+  EXPECT_EQ(pointer_events[1].timestamp, 1234000u);
+  EXPECT_EQ(pointer_events[1].x, 4.0);
+  EXPECT_EQ(pointer_events[1].y, 8.0);
+  EXPECT_EQ(pointer_events[1].device_kind, kFlutterPointerDeviceKindMouse);
+  EXPECT_EQ(pointer_events[1].buttons, kFlutterPointerButtonMousePrimary);
+  EXPECT_EQ(pointer_events[1].view_id, 42);
+  EXPECT_EQ(pointer_events[2].timestamp, 1235000u);
+  EXPECT_EQ(pointer_events[2].x, 5.0);
+  EXPECT_EQ(pointer_events[2].y, 9.0);
+  EXPECT_EQ(pointer_events[2].device_kind, kFlutterPointerDeviceKindMouse);
+  EXPECT_EQ(pointer_events[2].buttons, 0);
+  EXPECT_EQ(pointer_events[2].view_id, 42);
+}
+
+TEST(FlPointerManagerTest, NoButtonPressButtonRelease) {
+  g_autoptr(FlEngine) engine = make_mock_engine();
+  std::vector<FlutterPointerEvent> pointer_events;
+  log_pointer_events(engine, pointer_events);
+
+  g_autoptr(FlPointerManager) manager = fl_pointer_manager_new(42, engine);
+  // Release without associated press, will be ignored
+  fl_pointer_manager_handle_button_release(
+      manager, 1235, kFlutterPointerDeviceKindMouse, 5.0, 9.0,
+      kFlutterPointerButtonMousePrimary);
+
+  EXPECT_EQ(pointer_events.size(), 0u);
+}
+
+TEST(FlPointerManagerTest, Motion) {
+  g_autoptr(FlEngine) engine = make_mock_engine();
+  std::vector<FlutterPointerEvent> pointer_events;
+  log_pointer_events(engine, pointer_events);
+
+  g_autoptr(FlPointerManager) manager = fl_pointer_manager_new(42, engine);
+  fl_pointer_manager_handle_motion(manager, 1234,
+                                   kFlutterPointerDeviceKindMouse, 1.0, 2.0);
+  fl_pointer_manager_handle_motion(manager, 1235,
+                                   kFlutterPointerDeviceKindMouse, 3.0, 4.0);
+  fl_pointer_manager_handle_motion(manager, 1236,
+                                   kFlutterPointerDeviceKindMouse, 5.0, 6.0);
+
+  EXPECT_EQ(pointer_events.size(), 4u);
+
+  // Ignore first synthetic enter event
+  EXPECT_EQ(pointer_events[1].timestamp, 1234000u);
+  EXPECT_EQ(pointer_events[1].x, 1.0);
+  EXPECT_EQ(pointer_events[1].y, 2.0);
+  EXPECT_EQ(pointer_events[1].device_kind, kFlutterPointerDeviceKindMouse);
+  EXPECT_EQ(pointer_events[1].buttons, 0);
+  EXPECT_EQ(pointer_events[1].view_id, 42);
+  EXPECT_EQ(pointer_events[2].timestamp, 1235000u);
+  EXPECT_EQ(pointer_events[2].x, 3.0);
+  EXPECT_EQ(pointer_events[2].y, 4.0);
+  EXPECT_EQ(pointer_events[2].device_kind, kFlutterPointerDeviceKindMouse);
+  EXPECT_EQ(pointer_events[2].buttons, 0);
+  EXPECT_EQ(pointer_events[2].view_id, 42);
+  EXPECT_EQ(pointer_events[3].timestamp, 1236000u);
+  EXPECT_EQ(pointer_events[3].x, 5.0);
+  EXPECT_EQ(pointer_events[3].y, 6.0);
+  EXPECT_EQ(pointer_events[3].device_kind, kFlutterPointerDeviceKindMouse);
+  EXPECT_EQ(pointer_events[3].buttons, 0);
+  EXPECT_EQ(pointer_events[3].view_id, 42);
+}
+
+TEST(FlPointerManagerTest, Drag) {
+  g_autoptr(FlEngine) engine = make_mock_engine();
+  std::vector<FlutterPointerEvent> pointer_events;
+  log_pointer_events(engine, pointer_events);
+
+  g_autoptr(FlPointerManager) manager = fl_pointer_manager_new(42, engine);
+  fl_pointer_manager_handle_motion(manager, 1234,
+                                   kFlutterPointerDeviceKindMouse, 1.0, 2.0);
+  fl_pointer_manager_handle_button_press(
+      manager, 1235, kFlutterPointerDeviceKindMouse, 3.0, 4.0,
+      kFlutterPointerButtonMousePrimary);
+  fl_pointer_manager_handle_motion(manager, 1236,
+                                   kFlutterPointerDeviceKindMouse, 5.0, 6.0);
+  fl_pointer_manager_handle_button_release(
+      manager, 1237, kFlutterPointerDeviceKindMouse, 7.0, 8.0,
+      kFlutterPointerButtonMousePrimary);
+  fl_pointer_manager_handle_motion(manager, 1238,
+                                   kFlutterPointerDeviceKindMouse, 9.0, 10.0);
+
+  EXPECT_EQ(pointer_events.size(), 6u);
+
+  // Ignore first synthetic enter event
+  EXPECT_EQ(pointer_events[1].timestamp, 1234000u);
+  EXPECT_EQ(pointer_events[1].x, 1.0);
+  EXPECT_EQ(pointer_events[1].y, 2.0);
+  EXPECT_EQ(pointer_events[1].buttons, 0);
+  EXPECT_EQ(pointer_events[1].view_id, 42);
+  EXPECT_EQ(pointer_events[2].timestamp, 1235000u);
+  EXPECT_EQ(pointer_events[2].x, 3.0);
+  EXPECT_EQ(pointer_events[2].y, 4.0);
+  EXPECT_EQ(pointer_events[2].buttons, kFlutterPointerButtonMousePrimary);
+  EXPECT_EQ(pointer_events[2].view_id, 42);
+  EXPECT_EQ(pointer_events[3].timestamp, 1236000u);
+  EXPECT_EQ(pointer_events[3].x, 5.0);
+  EXPECT_EQ(pointer_events[3].y, 6.0);
+  EXPECT_EQ(pointer_events[3].buttons, kFlutterPointerButtonMousePrimary);
+  EXPECT_EQ(pointer_events[3].view_id, 42);
+  EXPECT_EQ(pointer_events[4].timestamp, 1237000u);
+  EXPECT_EQ(pointer_events[4].x, 7.0);
+  EXPECT_EQ(pointer_events[4].y, 8.0);
+  EXPECT_EQ(pointer_events[4].buttons, 0);
+  EXPECT_EQ(pointer_events[4].view_id, 42);
+  EXPECT_EQ(pointer_events[5].timestamp, 1238000u);
+  EXPECT_EQ(pointer_events[5].x, 9.0);
+  EXPECT_EQ(pointer_events[5].y, 10.0);
+  EXPECT_EQ(pointer_events[5].buttons, 0);
+  EXPECT_EQ(pointer_events[5].view_id, 42);
+}
+
+TEST(FlPointerManagerTest, DeviceKind) {
+  g_autoptr(FlEngine) engine = make_mock_engine();
+  std::vector<FlutterPointerEvent> pointer_events;
+  log_pointer_events(engine, pointer_events);
+
+  g_autoptr(FlPointerManager) manager = fl_pointer_manager_new(42, engine);
+  fl_pointer_manager_handle_enter(manager, 1234,
+                                  kFlutterPointerDeviceKindTrackpad, 1.0, 2.0);
+  fl_pointer_manager_handle_button_press(
+      manager, 1235, kFlutterPointerDeviceKindTrackpad, 1.0, 2.0,
+      kFlutterPointerButtonMousePrimary);
+  fl_pointer_manager_handle_motion(manager, 1238,
+                                   kFlutterPointerDeviceKindTrackpad, 3.0, 4.0);
+  fl_pointer_manager_handle_button_release(
+      manager, 1237, kFlutterPointerDeviceKindTrackpad, 3.0, 4.0,
+      kFlutterPointerButtonMousePrimary);
+  fl_pointer_manager_handle_leave(manager, 1235,
+                                  kFlutterPointerDeviceKindTrackpad, 3.0, 4.0);
+
+  EXPECT_EQ(pointer_events.size(), 5u);
+
+  EXPECT_EQ(pointer_events[0].device_kind, kFlutterPointerDeviceKindTrackpad);
+  EXPECT_EQ(pointer_events[1].device_kind, kFlutterPointerDeviceKindTrackpad);
+  EXPECT_EQ(pointer_events[2].device_kind, kFlutterPointerDeviceKindTrackpad);
+  EXPECT_EQ(pointer_events[3].device_kind, kFlutterPointerDeviceKindTrackpad);
+  EXPECT_EQ(pointer_events[4].device_kind, kFlutterPointerDeviceKindTrackpad);
+}
diff --git a/shell/platform/linux/fl_view.cc b/shell/platform/linux/fl_view.cc
index 6a07c3c..92a172b 100644
--- a/shell/platform/linux/fl_view.cc
+++ b/shell/platform/linux/fl_view.cc
@@ -18,6 +18,7 @@
 #include "flutter/shell/platform/linux/fl_keyboard_view_delegate.h"
 #include "flutter/shell/platform/linux/fl_mouse_cursor_handler.h"
 #include "flutter/shell/platform/linux/fl_plugin_registrar_private.h"
+#include "flutter/shell/platform/linux/fl_pointer_manager.h"
 #include "flutter/shell/platform/linux/fl_renderer_gdk.h"
 #include "flutter/shell/platform/linux/fl_scrolling_manager.h"
 #include "flutter/shell/platform/linux/fl_socket_accessible.h"
@@ -28,8 +29,6 @@
 #include "flutter/shell/platform/linux/public/flutter_linux/fl_engine.h"
 #include "flutter/shell/platform/linux/public/flutter_linux/fl_plugin_registry.h"
 
-static constexpr int kMicrosecondsPerMillisecond = 1000;
-
 struct _FlView {
   GtkBox parent_instance;
 
@@ -54,15 +53,15 @@
   // TRUE if have got the first frame to render.
   gboolean have_first_frame;
 
-  // Pointer button state recorded for sending status updates.
-  int64_t button_state;
-
   // Monitor to track window state.
   FlWindowStateMonitor* window_state_monitor;
 
   // Manages scrolling events.
   FlScrollingManager* scrolling_manager;
 
+  // Manages pointer events.
+  FlPointerManager* pointer_manager;
+
   // Manages keyboard events.
   FlKeyboardManager* keyboard_manager;
 
@@ -71,9 +70,6 @@
   FlTextInputHandler* text_input_handler;
   FlMouseCursorHandler* mouse_cursor_handler;
 
-  // TRUE if the mouse pointer is inside the view.
-  gboolean pointer_inside;
-
   // Accessible tree from Flutter, exposed as an AtkPlug.
   FlViewAccessible* view_accessible;
 
@@ -169,85 +165,23 @@
   }
 }
 
-// Converts a GDK button event into a Flutter event and sends it to the engine.
-static gboolean send_pointer_button_event(FlView* self, GdkEvent* event) {
-  guint event_time = gdk_event_get_time(event);
-  GdkEventType event_type = gdk_event_get_event_type(event);
-  GdkModifierType event_state = static_cast<GdkModifierType>(0);
-  gdk_event_get_state(event, &event_state);
+static gboolean get_mouse_button(GdkEvent* event, int64_t* button) {
   guint event_button = 0;
   gdk_event_get_button(event, &event_button);
-  gdouble event_x = 0.0, event_y = 0.0;
-  gdk_event_get_coords(event, &event_x, &event_y);
 
-  int64_t button;
   switch (event_button) {
     case 1:
-      button = kFlutterPointerButtonMousePrimary;
-      break;
+      *button = kFlutterPointerButtonMousePrimary;
+      return TRUE;
     case 2:
-      button = kFlutterPointerButtonMouseMiddle;
-      break;
+      *button = kFlutterPointerButtonMouseMiddle;
+      return TRUE;
     case 3:
-      button = kFlutterPointerButtonMouseSecondary;
-      break;
+      *button = kFlutterPointerButtonMouseSecondary;
+      return TRUE;
     default:
       return FALSE;
   }
-  int old_button_state = self->button_state;
-  FlutterPointerPhase phase = kMove;
-  if (event_type == GDK_BUTTON_PRESS) {
-    // Drop the event if Flutter already thinks the button is down.
-    if ((self->button_state & button) != 0) {
-      return FALSE;
-    }
-    self->button_state ^= button;
-
-    phase = old_button_state == 0 ? kDown : kMove;
-  } else if (event_type == GDK_BUTTON_RELEASE) {
-    // Drop the event if Flutter already thinks the button is up.
-    if ((self->button_state & button) == 0) {
-      return FALSE;
-    }
-    self->button_state ^= button;
-
-    phase = self->button_state == 0 ? kUp : kMove;
-  }
-
-  if (self->engine == nullptr) {
-    return FALSE;
-  }
-
-  gint scale_factor = gtk_widget_get_scale_factor(GTK_WIDGET(self));
-  fl_scrolling_manager_set_last_mouse_position(
-      self->scrolling_manager, event_x * scale_factor, event_y * scale_factor);
-  fl_keyboard_manager_sync_modifier_if_needed(self->keyboard_manager,
-                                              event_state, event_time);
-  fl_engine_send_mouse_pointer_event(
-      self->engine, self->view_id, phase,
-      event_time * kMicrosecondsPerMillisecond, event_x * scale_factor,
-      event_y * scale_factor, get_device_kind((GdkEvent*)event), 0, 0,
-      self->button_state);
-
-  return TRUE;
-}
-
-// Generates a mouse pointer event if the pointer appears inside the window.
-static void check_pointer_inside(FlView* self, GdkEvent* event) {
-  if (!self->pointer_inside) {
-    self->pointer_inside = TRUE;
-
-    gdouble x, y;
-    if (gdk_event_get_coords(event, &x, &y)) {
-      gint scale_factor = gtk_widget_get_scale_factor(GTK_WIDGET(self));
-
-      fl_engine_send_mouse_pointer_event(
-          self->engine, self->view_id, kAdd,
-          gdk_event_get_time(event) * kMicrosecondsPerMillisecond,
-          x * scale_factor, y * scale_factor, get_device_kind(event), 0, 0,
-          self->button_state);
-    }
-  }
 }
 
 // Updates the engine with the current window metrics.
@@ -374,6 +308,20 @@
   };
 }
 
+static void sync_modifier_if_needed(FlView* self, GdkEvent* event) {
+  guint event_time = gdk_event_get_time(event);
+  GdkModifierType event_state = static_cast<GdkModifierType>(0);
+  gdk_event_get_state(event, &event_state);
+  fl_keyboard_manager_sync_modifier_if_needed(self->keyboard_manager,
+                                              event_state, event_time);
+}
+
+static void set_scrolling_position(FlView* self, gdouble x, gdouble y) {
+  gint scale_factor = gtk_widget_get_scale_factor(GTK_WIDGET(self));
+  fl_scrolling_manager_set_last_mouse_position(
+      self->scrolling_manager, x * scale_factor, y * scale_factor);
+}
+
 // Signal handler for GtkWidget::button-press-event
 static gboolean button_press_event_cb(FlView* self,
                                       GdkEventButton* button_event) {
@@ -386,16 +334,43 @@
     return FALSE;
   }
 
-  check_pointer_inside(self, event);
+  int64_t button;
+  if (!get_mouse_button(event, &button)) {
+    return FALSE;
+  }
 
-  return send_pointer_button_event(self, event);
+  gdouble x = 0.0, y = 0.0;
+  gdk_event_get_coords(event, &x, &y);
+
+  set_scrolling_position(self, x, y);
+  sync_modifier_if_needed(self, event);
+
+  gint scale_factor = gtk_widget_get_scale_factor(GTK_WIDGET(self));
+  return fl_pointer_manager_handle_button_press(
+      self->pointer_manager, gdk_event_get_time(event), get_device_kind(event),
+      x * scale_factor, y * scale_factor, button);
 }
 
 // Signal handler for GtkWidget::button-release-event
 static gboolean button_release_event_cb(FlView* self,
                                         GdkEventButton* button_event) {
   GdkEvent* event = reinterpret_cast<GdkEvent*>(button_event);
-  return send_pointer_button_event(self, event);
+
+  int64_t button;
+  if (!get_mouse_button(event, &button)) {
+    return FALSE;
+  }
+
+  gdouble x = 0.0, y = 0.0;
+  gdk_event_get_coords(event, &x, &y);
+
+  set_scrolling_position(self, x, y);
+  sync_modifier_if_needed(self, event);
+
+  gint scale_factor = gtk_widget_get_scale_factor(GTK_WIDGET(self));
+  return fl_pointer_manager_handle_button_release(
+      self->pointer_manager, gdk_event_get_time(event), get_device_kind(event),
+      x * scale_factor, y * scale_factor, button);
 }
 
 // Signal handler for GtkWidget::scroll-event
@@ -413,77 +388,42 @@
 static gboolean motion_notify_event_cb(FlView* self,
                                        GdkEventMotion* motion_event) {
   GdkEvent* event = reinterpret_cast<GdkEvent*>(motion_event);
+  sync_modifier_if_needed(self, event);
 
-  if (self->engine == nullptr) {
-    return FALSE;
-  }
-
-  guint event_time = gdk_event_get_time(event);
-  GdkModifierType event_state = static_cast<GdkModifierType>(0);
-  gdk_event_get_state(event, &event_state);
-  gdouble event_x = 0.0, event_y = 0.0;
-  gdk_event_get_coords(event, &event_x, &event_y);
-
-  check_pointer_inside(self, event);
-
+  gdouble x = 0.0, y = 0.0;
+  gdk_event_get_coords(event, &x, &y);
   gint scale_factor = gtk_widget_get_scale_factor(GTK_WIDGET(self));
-
-  fl_keyboard_manager_sync_modifier_if_needed(self->keyboard_manager,
-                                              event_state, event_time);
-  fl_engine_send_mouse_pointer_event(
-      self->engine, self->view_id, self->button_state != 0 ? kMove : kHover,
-      event_time * kMicrosecondsPerMillisecond, event_x * scale_factor,
-      event_y * scale_factor, get_device_kind((GdkEvent*)event), 0, 0,
-      self->button_state);
-
-  return TRUE;
+  return fl_pointer_manager_handle_motion(
+      self->pointer_manager, gdk_event_get_time(event), get_device_kind(event),
+      x * scale_factor, y * scale_factor);
 }
 
 // Signal handler for GtkWidget::enter-notify-event
 static gboolean enter_notify_event_cb(FlView* self,
                                       GdkEventCrossing* crossing_event) {
   GdkEvent* event = reinterpret_cast<GdkEvent*>(crossing_event);
-
-  if (self->engine == nullptr) {
-    return FALSE;
-  }
-
-  check_pointer_inside(self, event);
-
-  return TRUE;
+  gdouble x = 0.0, y = 0.0;
+  gdk_event_get_coords(event, &x, &y);
+  gint scale_factor = gtk_widget_get_scale_factor(GTK_WIDGET(self));
+  return fl_pointer_manager_handle_enter(
+      self->pointer_manager, gdk_event_get_time(event), get_device_kind(event),
+      x * scale_factor, y * scale_factor);
 }
 
 // Signal handler for GtkWidget::leave-notify-event
 static gboolean leave_notify_event_cb(FlView* self,
                                       GdkEventCrossing* crossing_event) {
-  GdkEvent* event = reinterpret_cast<GdkEvent*>(crossing_event);
-
-  guint event_time = gdk_event_get_time(event);
-  gdouble event_x = 0.0, event_y = 0.0;
-  gdk_event_get_coords(event, &event_x, &event_y);
-
   if (crossing_event->mode != GDK_CROSSING_NORMAL) {
     return FALSE;
   }
 
-  if (self->engine == nullptr) {
-    return FALSE;
-  }
-
-  // Don't remove pointer while button is down; In case of dragging outside of
-  // window with mouse grab active Gtk will send another leave notify on
-  // release.
-  if (self->pointer_inside && self->button_state == 0) {
-    gint scale_factor = gtk_widget_get_scale_factor(GTK_WIDGET(self));
-    fl_engine_send_mouse_pointer_event(
-        self->engine, self->view_id, kRemove,
-        event_time * kMicrosecondsPerMillisecond, event_x * scale_factor,
-        event_y * scale_factor, get_device_kind((GdkEvent*)event), 0, 0,
-        self->button_state);
-    self->pointer_inside = FALSE;
-  }
-
-  return TRUE;
+  GdkEvent* event = reinterpret_cast<GdkEvent*>(crossing_event);
+  gdouble x = 0.0, y = 0.0;
+  gdk_event_get_coords(event, &x, &y);
+  gint scale_factor = gtk_widget_get_scale_factor(GTK_WIDGET(self));
+  return fl_pointer_manager_handle_leave(
+      self->pointer_manager, gdk_event_get_time(event), get_device_kind(event),
+      x * scale_factor, y * scale_factor);
 }
 
 static void gesture_rotation_begin_cb(FlView* self) {
@@ -646,6 +586,7 @@
   g_clear_pointer(&self->background_color, gdk_rgba_free);
   g_clear_object(&self->window_state_monitor);
   g_clear_object(&self->scrolling_manager);
+  g_clear_object(&self->pointer_manager);
   g_clear_object(&self->keyboard_manager);
   g_clear_object(&self->keyboard_handler);
   g_clear_object(&self->mouse_cursor_handler);
@@ -666,21 +607,24 @@
 }
 
 // Implements GtkWidget::key_press_event.
-static gboolean fl_view_key_press_event(GtkWidget* widget, GdkEventKey* event) {
+static gboolean fl_view_key_press_event(GtkWidget* widget,
+                                        GdkEventKey* key_event) {
   FlView* self = FL_VIEW(widget);
 
+  GdkEvent* event = reinterpret_cast<GdkEvent*>(key_event);
   return fl_keyboard_manager_handle_event(
-      self->keyboard_manager, fl_key_event_new_from_gdk_event(gdk_event_copy(
-                                  reinterpret_cast<GdkEvent*>(event))));
+      self->keyboard_manager,
+      fl_key_event_new_from_gdk_event(gdk_event_copy(event)));
 }
 
 // Implements GtkWidget::key_release_event.
 static gboolean fl_view_key_release_event(GtkWidget* widget,
-                                          GdkEventKey* event) {
+                                          GdkEventKey* key_event) {
   FlView* self = FL_VIEW(widget);
+  GdkEvent* event = reinterpret_cast<GdkEvent*>(key_event);
   return fl_keyboard_manager_handle_event(
-      self->keyboard_manager, fl_key_event_new_from_gdk_event(gdk_event_copy(
-                                  reinterpret_cast<GdkEvent*>(event))));
+      self->keyboard_manager,
+      fl_key_event_new_from_gdk_event(gdk_event_copy(event)));
 }
 
 static void fl_view_class_init(FlViewClass* klass) {
@@ -769,6 +713,8 @@
   g_assert(FL_IS_RENDERER_GDK(renderer));
   self->renderer = FL_RENDERER_GDK(g_object_ref(renderer));
 
+  self->pointer_manager = fl_pointer_manager_new(self->view_id, engine);
+
   fl_engine_set_update_semantics_handler(self->engine, update_semantics_cb,
                                          self, nullptr);
   self->on_pre_engine_restart_cb_id =
@@ -802,6 +748,8 @@
   fl_renderer_add_renderable(FL_RENDERER(self->renderer), self->view_id,
                              FL_RENDERABLE(self));
 
+  self->pointer_manager = fl_pointer_manager_new(self->view_id, engine);
+
   return self;
 }