Correctly synthesise event buttons (#30535)

* Correctly synthesise buttons, and add tests
diff --git a/packages/flutter/lib/src/gestures/converter.dart b/packages/flutter/lib/src/gestures/converter.dart
index 5bb120e..fc71a68 100644
--- a/packages/flutter/lib/src/gestures/converter.dart
+++ b/packages/flutter/lib/src/gestures/converter.dart
@@ -40,6 +40,22 @@
   }
 }
 
+// Add `kPrimaryButton` to [buttons] when a pointer of certain devices is down.
+//
+// TODO(tongmu): This patch is supposed to be done by embedders. Patching it
+// in framework is a workaround before [PointerEventConverter] is moved to embedders.
+// https://github.com/flutter/flutter/issues/30454
+int _synthesiseDownButtons(int buttons, PointerDeviceKind kind) {
+  switch (kind) {
+    case PointerDeviceKind.touch:
+    case PointerDeviceKind.stylus:
+    case PointerDeviceKind.invertedStylus:
+      return buttons | kPrimaryButton;
+    default:
+      return buttons;
+  }
+}
+
 /// Converts from engine pointer data to framework pointer events.
 ///
 /// This takes [PointerDataPacket] objects, as received from the engine via
@@ -207,8 +223,7 @@
               kind: kind,
               device: datum.device,
               position: position,
-              // TODO(tongmu): Move button patching to embedder, https://github.com/flutter/flutter/issues/30454
-              buttons: datum.buttons | kPrimaryButton,
+              buttons: _synthesiseDownButtons(datum.buttons, kind),
               obscured: datum.obscured,
               pressure: datum.pressure,
               pressureMin: datum.pressureMin,
@@ -237,8 +252,7 @@
               device: datum.device,
               position: position,
               delta: state.deltaTo(position),
-              // TODO(tongmu): Move button patching to embedder, https://github.com/flutter/flutter/issues/30454
-              buttons: datum.buttons | kPrimaryButton,
+              buttons: _synthesiseDownButtons(datum.buttons, kind),
               obscured: datum.obscured,
               pressure: datum.pressure,
               pressureMin: datum.pressureMin,
@@ -273,8 +287,7 @@
                 device: datum.device,
                 position: position,
                 delta: state.deltaTo(position),
-                // TODO(tongmu): Move button patching to embedder, https://github.com/flutter/flutter/issues/30454
-                buttons: datum.buttons | kPrimaryButton,
+                buttons: _synthesiseDownButtons(datum.buttons, kind),
                 obscured: datum.obscured,
                 pressure: datum.pressure,
                 pressureMin: datum.pressureMin,
@@ -421,8 +434,7 @@
                   device: datum.device,
                   position: position,
                   delta: state.deltaTo(position),
-                  // TODO(tongmu): Move button patching to embedder, https://github.com/flutter/flutter/issues/30454
-                  buttons: datum.buttons | kPrimaryButton,
+                  buttons: _synthesiseDownButtons(datum.buttons, kind),
                   obscured: datum.obscured,
                   pressure: datum.pressure,
                   pressureMin: datum.pressureMin,
diff --git a/packages/flutter/test/gestures/gesture_binding_test.dart b/packages/flutter/test/gestures/gesture_binding_test.dart
index bd30ac3..37dfbab 100644
--- a/packages/flutter/test/gestures/gesture_binding_test.dart
+++ b/packages/flutter/test/gestures/gesture_binding_test.dart
@@ -47,7 +47,6 @@
     ui.window.onPointerDataPacket(packet);
     expect(events.length, 2);
     expect(events[0].runtimeType, equals(PointerDownEvent));
-    expect(events[0].buttons, equals(1));
     expect(events[1].runtimeType, equals(PointerUpEvent));
   });
 
@@ -66,9 +65,7 @@
     ui.window.onPointerDataPacket(packet);
     expect(events.length, 3);
     expect(events[0].runtimeType, equals(PointerDownEvent));
-    expect(events[0].buttons, equals(1));
     expect(events[1].runtimeType, equals(PointerMoveEvent));
-    expect(events[1].buttons, equals(1));
     expect(events[2].runtimeType, equals(PointerUpEvent));
   });
 
@@ -122,9 +119,7 @@
     ui.window.onPointerDataPacket(packet);
     expect(events.length, 3);
     expect(events[0].runtimeType, equals(PointerDownEvent));
-    expect(events[0].buttons, equals(1));
     expect(events[1].runtimeType, equals(PointerMoveEvent));
-    expect(events[1].buttons, equals(1));
     expect(events[1].delta, equals(const Offset(9.0, 12.0)));
     expect(events[2].runtimeType, equals(PointerUpEvent));
   });
@@ -143,7 +138,6 @@
     ui.window.onPointerDataPacket(packet);
     expect(events.length, 2);
     expect(events[0].runtimeType, equals(PointerDownEvent));
-    expect(events[0].buttons, equals(1));
     expect(events[1].runtimeType, equals(PointerCancelEvent));
   });
 
@@ -165,7 +159,6 @@
     ui.window.onPointerDataPacket(packet);
     expect(events.length, 2);
     expect(events[0].runtimeType, equals(PointerDownEvent));
-    expect(events[0].buttons, equals(1));
     expect(events[1].runtimeType, equals(PointerCancelEvent));
   });
 
@@ -207,7 +200,6 @@
     expect(events[1].runtimeType, equals(PointerHoverEvent));
     expect(events[1].delta, equals(const Offset(5.0, 7.0)));
     expect(events[2].runtimeType, equals(PointerDownEvent));
-    expect(events[2].buttons, equals(1));
     expect(events[3].runtimeType, equals(PointerCancelEvent));
     expect(events[4].runtimeType, equals(PointerHoverEvent));
     expect(events[5].runtimeType, equals(PointerRemovedEvent));
@@ -254,10 +246,109 @@
     expect(events[2].runtimeType, equals(PointerScrollEvent));
     expect(events[3].runtimeType, equals(PointerHoverEvent));
     expect(events[4].runtimeType, equals(PointerDownEvent));
-    expect(events[4].buttons, equals(1));
     expect(events[5].runtimeType, equals(PointerMoveEvent));
-    expect(events[5].buttons, equals(1));
     expect(events[5].delta, equals(unexpectedOffset));
     expect(events[6].runtimeType, equals(PointerScrollEvent));
   });
+
+  test('Should synthesise kPrimaryButton for touch', () {
+    final Offset location = const Offset(10.0, 10.0) * ui.window.devicePixelRatio;
+    const PointerDeviceKind kind = PointerDeviceKind.touch;
+    final ui.PointerDataPacket packet = ui.PointerDataPacket(
+      data: <ui.PointerData>[
+        ui.PointerData(change: ui.PointerChange.add, kind: kind, physicalX: location.dx, physicalY: location.dy),
+        ui.PointerData(change: ui.PointerChange.hover, kind: kind, physicalX: location.dx, physicalY: location.dy),
+        ui.PointerData(change: ui.PointerChange.down, kind: kind, physicalX: location.dx, physicalY: location.dy),
+        ui.PointerData(change: ui.PointerChange.move, kind: kind, physicalX: location.dx, physicalY: location.dy),
+        ui.PointerData(change: ui.PointerChange.up, kind: kind, physicalX: location.dx, physicalY: location.dy),
+      ]
+    );
+
+    final List<PointerEvent> events = PointerEventConverter.expand(
+      packet.data, ui.window.devicePixelRatio).toList();
+
+    expect(events.length, 5);
+    expect(events[0].runtimeType, equals(PointerAddedEvent));
+    expect(events[0].buttons, equals(0));
+    expect(events[1].runtimeType, equals(PointerHoverEvent));
+    expect(events[1].buttons, equals(0));
+    expect(events[2].runtimeType, equals(PointerDownEvent));
+    expect(events[2].buttons, equals(kPrimaryButton));
+    expect(events[3].runtimeType, equals(PointerMoveEvent));
+    expect(events[3].buttons, equals(kPrimaryButton));
+    expect(events[4].runtimeType, equals(PointerUpEvent));
+    expect(events[4].buttons, equals(0));
+
+    PointerEventConverter.clearPointers();
+  });
+
+  test('Should synthesise kPrimaryButton for stylus', () {
+    final Offset location = const Offset(10.0, 10.0) * ui.window.devicePixelRatio;
+    for (PointerDeviceKind kind in <PointerDeviceKind>[
+      PointerDeviceKind.stylus,
+      PointerDeviceKind.invertedStylus,
+    ]) {
+
+      final ui.PointerDataPacket packet = ui.PointerDataPacket(
+        data: <ui.PointerData>[
+          ui.PointerData(change: ui.PointerChange.add, kind: kind, physicalX: location.dx, physicalY: location.dy),
+          ui.PointerData(change: ui.PointerChange.hover, kind: kind, physicalX: location.dx, physicalY: location.dy),
+          ui.PointerData(change: ui.PointerChange.down, kind: kind, physicalX: location.dx, physicalY: location.dy),
+          ui.PointerData(change: ui.PointerChange.move, buttons: kSecondaryStylusButton, kind: kind, physicalX: location.dx, physicalY: location.dy),
+          ui.PointerData(change: ui.PointerChange.up, kind: kind, physicalX: location.dx, physicalY: location.dy),
+        ]
+      );
+
+      final List<PointerEvent> events = PointerEventConverter.expand(
+        packet.data, ui.window.devicePixelRatio).toList();
+
+      expect(events.length, 5);
+      expect(events[0].runtimeType, equals(PointerAddedEvent));
+      expect(events[0].buttons, equals(0));
+      expect(events[1].runtimeType, equals(PointerHoverEvent));
+      expect(events[1].buttons, equals(0));
+      expect(events[2].runtimeType, equals(PointerDownEvent));
+      expect(events[2].buttons, equals(kPrimaryButton));
+      expect(events[3].runtimeType, equals(PointerMoveEvent));
+      expect(events[3].buttons, equals(kPrimaryButton | kSecondaryStylusButton));
+      expect(events[4].runtimeType, equals(PointerUpEvent));
+      expect(events[4].buttons, equals(0));
+
+      PointerEventConverter.clearPointers();
+    }
+  });
+
+  test('Should not synthesise kPrimaryButton for certain devices', () {
+    final Offset location = const Offset(10.0, 10.0) * ui.window.devicePixelRatio;
+    for (PointerDeviceKind kind in <PointerDeviceKind>[
+      PointerDeviceKind.mouse,
+    ]) {
+      final ui.PointerDataPacket packet = ui.PointerDataPacket(
+        data: <ui.PointerData>[
+          ui.PointerData(change: ui.PointerChange.add, kind: kind, physicalX: location.dx, physicalY: location.dy),
+          ui.PointerData(change: ui.PointerChange.hover, kind: kind, physicalX: location.dx, physicalY: location.dy),
+          ui.PointerData(change: ui.PointerChange.down, kind: kind, buttons: kMiddleMouseButton, physicalX: location.dx, physicalY: location.dy),
+          ui.PointerData(change: ui.PointerChange.move, kind: kind, buttons: kMiddleMouseButton | kSecondaryMouseButton, physicalX: location.dx, physicalY: location.dy),
+          ui.PointerData(change: ui.PointerChange.up, kind: kind, physicalX: location.dx, physicalY: location.dy),
+        ]
+      );
+
+      final List<PointerEvent> events = PointerEventConverter.expand(
+        packet.data, ui.window.devicePixelRatio).toList();
+
+      expect(events.length, 5);
+      expect(events[0].runtimeType, equals(PointerAddedEvent));
+      expect(events[0].buttons, equals(0));
+      expect(events[1].runtimeType, equals(PointerHoverEvent));
+      expect(events[1].buttons, equals(0));
+      expect(events[2].runtimeType, equals(PointerDownEvent));
+      expect(events[2].buttons, equals(kMiddleMouseButton));
+      expect(events[3].runtimeType, equals(PointerMoveEvent));
+      expect(events[3].buttons, equals(kMiddleMouseButton | kSecondaryMouseButton));
+      expect(events[4].runtimeType, equals(PointerUpEvent));
+      expect(events[4].buttons, equals(0));
+
+      PointerEventConverter.clearPointers();
+    }
+  });
 }