Add scroll wheel support to desktop GLFW shell (#8416)

Sends scroll events from GLFW to the Flutter engine, allowing scrolling
of Scrollables via a scroll wheel.
diff --git a/shell/platform/glfw/flutter_glfw.cc b/shell/platform/glfw/flutter_glfw.cc
index ab03cac..98e76f0 100644
--- a/shell/platform/glfw/flutter_glfw.cc
+++ b/shell/platform/glfw/flutter_glfw.cc
@@ -155,57 +155,86 @@
   FlutterEngineSendWindowMetricsEvent(state->engine, &event);
 }
 
-// Sends a pointer event to the Flutter engine with the given phase.
-static void SendPointerEventWithPhase(GLFWwindow* window,
-                                      FlutterPointerPhase phase,
-                                      double x,
-                                      double y) {
-  auto state = GetSavedWindowState(window);
+// Sends a pointer event to the Flutter engine based on the given data.
+//
+// Any coordinate/distance values in |event_data| should be in screen
+// coordinates; they will be adjusted to pixel values before being sent.
+static void SendPointerEventWithData(GLFWwindow* window,
+                                     const FlutterPointerEvent& event_data) {
+  auto* state = GetSavedWindowState(window);
   // If sending anything other than an add, and the pointer isn't already added,
   // synthesize an add to satisfy Flutter's expectations about events.
-  if (!state->pointer_currently_added && phase != FlutterPointerPhase::kAdd) {
-    SendPointerEventWithPhase(window, FlutterPointerPhase::kAdd, x, y);
+  if (!state->pointer_currently_added &&
+      event_data.phase != FlutterPointerPhase::kAdd) {
+    FlutterPointerEvent event = {};
+    event.phase = FlutterPointerPhase::kAdd;
+    event.x = event_data.x;
+    event.y = event_data.y;
+    SendPointerEventWithData(window, event);
   }
   // Don't double-add (e.g., if events are delivered out of order, so an add has
   // already been synthesized).
-  if (state->pointer_currently_added && phase == FlutterPointerPhase::kAdd) {
+  if (state->pointer_currently_added &&
+      event_data.phase == FlutterPointerPhase::kAdd) {
     return;
   }
 
-  FlutterPointerEvent event = {};
+  FlutterPointerEvent event = event_data;
+  // Set metadata that's always the same regardless of the event.
   event.struct_size = sizeof(event);
-  event.phase = phase;
-  event.x = x * state->window_pixels_per_screen_coordinate;
-  event.y = y * state->window_pixels_per_screen_coordinate;
   event.timestamp =
       std::chrono::duration_cast<std::chrono::microseconds>(
           std::chrono::high_resolution_clock::now().time_since_epoch())
           .count();
+  // Convert all screen coordinates to pixel coordinates.
+  event.x *= state->window_pixels_per_screen_coordinate;
+  event.y *= state->window_pixels_per_screen_coordinate;
+  event.scroll_delta_x *= state->window_pixels_per_screen_coordinate;
+  event.scroll_delta_y *= state->window_pixels_per_screen_coordinate;
+
   FlutterEngineSendPointerEvent(state->engine, &event, 1);
 
-  if (phase == FlutterPointerPhase::kAdd) {
+  if (event_data.phase == FlutterPointerPhase::kAdd) {
     state->pointer_currently_added = true;
-  } else if (phase == FlutterPointerPhase::kRemove) {
+  } else if (event_data.phase == FlutterPointerPhase::kRemove) {
     state->pointer_currently_added = false;
   }
 }
 
+// Updates |event_data| with the current location of the mouse cursor.
+static void SetEventLocationFromCursorPosition(
+    GLFWwindow* window,
+    FlutterPointerEvent* event_data) {
+  glfwGetCursorPos(window, &event_data->x, &event_data->y);
+}
+
+// Set's |event_data|'s phase to either kMove or kHover depending on the current
+// primary mouse button state.
+static void SetEventPhaseFromCursorButtonState(
+    GLFWwindow* window,
+    FlutterPointerEvent* event_data) {
+  event_data->phase =
+      glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS
+          ? FlutterPointerPhase::kMove
+          : FlutterPointerPhase::kHover;
+}
+
 // Reports the mouse entering or leaving the Flutter view.
 static void GLFWCursorEnterCallback(GLFWwindow* window, int entered) {
-  double x, y;
-  glfwGetCursorPos(window, &x, &y);
-  FlutterPointerPhase phase =
+  FlutterPointerEvent event = {};
+  event.phase =
       entered ? FlutterPointerPhase::kAdd : FlutterPointerPhase::kRemove;
-  SendPointerEventWithPhase(window, phase, x, y);
+  SetEventLocationFromCursorPosition(window, &event);
+  SendPointerEventWithData(window, event);
 }
 
 // Reports mouse movement to the Flutter engine.
 static void GLFWCursorPositionCallback(GLFWwindow* window, double x, double y) {
-  bool button_down =
-      glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS;
-  FlutterPointerPhase phase =
-      button_down ? FlutterPointerPhase::kMove : FlutterPointerPhase::kHover;
-  SendPointerEventWithPhase(window, phase, x, y);
+  FlutterPointerEvent event = {};
+  event.x = x;
+  event.y = y;
+  SetEventPhaseFromCursorButtonState(window, &event);
+  SendPointerEventWithData(window, event);
 }
 
 // Reports mouse button press to the Flutter engine.
@@ -219,12 +248,11 @@
     return;
   }
 
-  double x, y;
-  glfwGetCursorPos(window, &x, &y);
-  FlutterPointerPhase phase = (action == GLFW_PRESS)
-                                  ? FlutterPointerPhase::kDown
-                                  : FlutterPointerPhase::kUp;
-  SendPointerEventWithPhase(window, phase, x, y);
+  FlutterPointerEvent event = {};
+  event.phase = (action == GLFW_PRESS) ? FlutterPointerPhase::kDown
+                                       : FlutterPointerPhase::kUp;
+  SetEventLocationFromCursorPosition(window, &event);
+  SendPointerEventWithData(window, event);
 
   // If mouse tracking isn't already enabled, turn it on for the duration of
   // the drag to generate kMove events.
@@ -242,6 +270,22 @@
   }
 }
 
+// Reports scroll wheel events to the Flutter engine.
+static void GLFWScrollCallback(GLFWwindow* window,
+                               double delta_x,
+                               double delta_y) {
+  FlutterPointerEvent event = {};
+  SetEventLocationFromCursorPosition(window, &event);
+  SetEventPhaseFromCursorButtonState(window, &event);
+  event.signal_kind = FlutterPointerSignalKind::kFlutterPointerSignalKindScroll;
+  // TODO: See if this can be queried from the OS; this value is chosen
+  // arbitrarily to get something that feels reasonable.
+  const int kScrollOffsetMultiplier = 20;
+  event.scroll_delta_x = delta_x * kScrollOffsetMultiplier;
+  event.scroll_delta_y = -delta_y * kScrollOffsetMultiplier;
+  SendPointerEventWithData(window, event);
+}
+
 // Passes character input events to registered handlers.
 static void GLFWCharCallback(GLFWwindow* window, unsigned int code_point) {
   for (const auto& handler :
@@ -276,6 +320,7 @@
   glfwSetKeyCallback(window, GLFWKeyCallback);
   glfwSetCharCallback(window, GLFWCharCallback);
   glfwSetMouseButtonCallback(window, GLFWMouseButtonCallback);
+  glfwSetScrollCallback(window, GLFWScrollCallback);
   if (GetSavedWindowState(window)->hover_tracking_enabled) {
     SetHoverCallbacksEnabled(window, true);
   }
@@ -286,6 +331,7 @@
   glfwSetKeyCallback(window, nullptr);
   glfwSetCharCallback(window, nullptr);
   glfwSetMouseButtonCallback(window, nullptr);
+  glfwSetScrollCallback(window, nullptr);
   SetHoverCallbacksEnabled(window, false);
 }