Fix scrollbar hit testing based on pointer device kind (#77755)

diff --git a/packages/flutter/lib/src/material/scrollbar.dart b/packages/flutter/lib/src/material/scrollbar.dart
index 58353d7..d7c1e1e 100644
--- a/packages/flutter/lib/src/material/scrollbar.dart
+++ b/packages/flutter/lib/src/material/scrollbar.dart
@@ -352,7 +352,7 @@
   void handleHover(PointerHoverEvent event) {
     super.handleHover(event);
     // Check if the position of the pointer falls over the painted scrollbar
-    if (isPointerOverScrollbar(event.position)) {
+    if (isPointerOverScrollbar(event.position, event.kind)) {
       // Pointer is hovering over the scrollbar
       setState(() { _hoverIsActive = true; });
       _hoverAnimationController.forward();
diff --git a/packages/flutter/lib/src/widgets/scrollbar.dart b/packages/flutter/lib/src/widgets/scrollbar.dart
index 41e127c..e6fbe10 100644
--- a/packages/flutter/lib/src/widgets/scrollbar.dart
+++ b/packages/flutter/lib/src/widgets/scrollbar.dart
@@ -478,9 +478,10 @@
     return _paintScrollbar(canvas, size, thumbExtent, _lastAxisDirection!);
   }
 
-  /// Same as hitTest, but includes some padding to make sure that the region
+  /// Same as hitTest, but includes some padding when the [PointerEvent] is
+  /// caused by [PointerDeviceKind.touch] to make sure that the region
   /// isn't too small to be interacted with by the user.
-  bool hitTestInteractive(Offset position) {
+  bool hitTestInteractive(Offset position, PointerDeviceKind kind) {
     if (_thumbRect == null) {
       return false;
     }
@@ -488,19 +489,25 @@
     if (fadeoutOpacityAnimation.value == 0.0) {
       return false;
     }
-    final Rect interactiveScrollbarRect = _trackRect == null
-      ? _thumbRect!.expandToInclude(
-          Rect.fromCircle(center: _thumbRect!.center, radius: _kMinInteractiveSize / 2),
-        )
-      : _trackRect!.expandToInclude(
+
+    final Rect interactiveRect = _trackRect ?? _thumbRect!;
+    switch (kind) {
+      case PointerDeviceKind.touch:
+        final Rect touchScrollbarRect = interactiveRect.expandToInclude(
           Rect.fromCircle(center: _thumbRect!.center, radius: _kMinInteractiveSize / 2),
         );
-    return interactiveScrollbarRect.contains(position);
+        return touchScrollbarRect.contains(position);
+      case PointerDeviceKind.mouse:
+      case PointerDeviceKind.stylus:
+      case PointerDeviceKind.invertedStylus:
+      case PointerDeviceKind.unknown:
+        return interactiveRect.contains(position);
+    }
   }
 
   /// Same as hitTestInteractive, but excludes the track portion of the scrollbar.
   /// Used to evaluate interactions with only the scrollbar thumb.
-  bool hitTestOnlyThumbInteractive(Offset position) {
+  bool hitTestOnlyThumbInteractive(Offset position, PointerDeviceKind kind) {
     if (_thumbRect == null) {
       return false;
     }
@@ -508,10 +515,19 @@
     if (fadeoutOpacityAnimation.value == 0.0) {
       return false;
     }
-    final Rect interactiveThumbRect = _thumbRect!.expandToInclude(
-      Rect.fromCircle(center: _thumbRect!.center, radius: _kMinInteractiveSize / 2),
-    );
-    return interactiveThumbRect.contains(position);
+
+    switch (kind) {
+      case PointerDeviceKind.touch:
+        final Rect touchThumbRect = _thumbRect!.expandToInclude(
+          Rect.fromCircle(center: _thumbRect!.center, radius: _kMinInteractiveSize / 2),
+        );
+        return touchThumbRect.contains(position);
+      case PointerDeviceKind.mouse:
+      case PointerDeviceKind.stylus:
+      case PointerDeviceKind.invertedStylus:
+      case PointerDeviceKind.unknown:
+        return _thumbRect!.contains(position);
+    }
   }
 
   // Scrollbars are interactive.
@@ -1153,33 +1169,33 @@
   ///
   /// Excludes the [RawScrollbar] thumb.
   @protected
-  bool isPointerOverTrack(Offset position) {
+  bool isPointerOverTrack(Offset position, PointerDeviceKind kind) {
     if (_scrollbarPainterKey.currentContext == null) {
       return false;
     }
     final Offset localOffset = _getLocalOffset(_scrollbarPainterKey, position);
-    return scrollbarPainter.hitTestInteractive(localOffset)
-      && !scrollbarPainter.hitTestOnlyThumbInteractive(localOffset);
+    return scrollbarPainter.hitTestInteractive(localOffset, kind)
+      && !scrollbarPainter.hitTestOnlyThumbInteractive(localOffset, kind);
   }
   /// Returns true if the provided [Offset] is located over the thumb of the
   /// [RawScrollbar].
   @protected
-  bool isPointerOverThumb(Offset position) {
+  bool isPointerOverThumb(Offset position, PointerDeviceKind kind) {
     if (_scrollbarPainterKey.currentContext == null) {
       return false;
     }
     final Offset localOffset = _getLocalOffset(_scrollbarPainterKey, position);
-    return scrollbarPainter.hitTestOnlyThumbInteractive(localOffset);
+    return scrollbarPainter.hitTestOnlyThumbInteractive(localOffset, kind);
   }
   /// Returns true if the provided [Offset] is located over the track or thumb
   /// of the [RawScrollbar].
   @protected
-  bool isPointerOverScrollbar(Offset position) {
+  bool isPointerOverScrollbar(Offset position, PointerDeviceKind kind) {
     if (_scrollbarPainterKey.currentContext == null) {
       return false;
     }
     final Offset localOffset = _getLocalOffset(_scrollbarPainterKey, position);
-    return scrollbarPainter.hitTestInteractive(localOffset);
+    return scrollbarPainter.hitTestInteractive(localOffset, kind);
   }
 
   /// Cancels the fade out animation so the scrollbar will remain visible for
@@ -1194,7 +1210,7 @@
   @mustCallSuper
   void handleHover(PointerHoverEvent event) {
     // Check if the position of the pointer falls over the painted scrollbar
-    if (isPointerOverScrollbar(event.position)) {
+    if (isPointerOverScrollbar(event.position, event.kind)) {
       _hoverIsActive = true;
       _fadeoutTimer?.cancel();
     } else if (_hoverIsActive) {
@@ -1292,20 +1308,20 @@
 
   @override
   bool isPointerAllowed(PointerDownEvent event) {
-    if (!_hitTestInteractive(_customPaintKey, event.position)) {
+    if (!_hitTestInteractive(_customPaintKey, event.position, event.kind)) {
       return false;
     }
     return super.isPointerAllowed(event);
   }
 
-  bool _hitTestInteractive(GlobalKey customPaintKey, Offset offset) {
+  bool _hitTestInteractive(GlobalKey customPaintKey, Offset offset, PointerDeviceKind kind) {
     if (customPaintKey.currentContext == null) {
       return false;
     }
     final CustomPaint customPaint = customPaintKey.currentContext!.widget as CustomPaint;
     final ScrollbarPainter painter = customPaint.foregroundPainter! as ScrollbarPainter;
     final Offset localOffset = _getLocalOffset(customPaintKey, offset);
-    return painter.hitTestOnlyThumbInteractive(localOffset);
+    return painter.hitTestOnlyThumbInteractive(localOffset, kind);
   }
 }
 
@@ -1322,13 +1338,13 @@
 
   @override
   bool isPointerAllowed(PointerDownEvent event) {
-    if (!_hitTestInteractive(_customPaintKey, event.position)) {
+    if (!_hitTestInteractive(_customPaintKey, event.position, event.kind)) {
       return false;
     }
     return super.isPointerAllowed(event);
   }
 
-  bool _hitTestInteractive(GlobalKey customPaintKey, Offset offset) {
+  bool _hitTestInteractive(GlobalKey customPaintKey, Offset offset, PointerDeviceKind kind) {
     if (customPaintKey.currentContext == null) {
       return false;
     }
@@ -1336,7 +1352,7 @@
     final ScrollbarPainter painter = customPaint.foregroundPainter! as ScrollbarPainter;
     final Offset localOffset = _getLocalOffset(customPaintKey, offset);
     // We only receive track taps that are not on the thumb.
-    return painter.hitTestInteractive(localOffset) && !painter.hitTestOnlyThumbInteractive(localOffset);
+    return painter.hitTestInteractive(localOffset, kind) && !painter.hitTestOnlyThumbInteractive(localOffset, kind);
   }
 }
 
diff --git a/packages/flutter/test/widgets/scrollbar_test.dart b/packages/flutter/test/widgets/scrollbar_test.dart
index 92f6f1a..4220c78 100644
--- a/packages/flutter/test/widgets/scrollbar_test.dart
+++ b/packages/flutter/test/widgets/scrollbar_test.dart
@@ -866,4 +866,93 @@
       paintsExactlyCountTimes(#drawRect, 2),
     );
   });
+
+  testWidgets('Scrollbar hit test area adjusts for PointerDeviceKind', (WidgetTester tester) async {
+    final ScrollController scrollController = ScrollController();
+    await tester.pumpWidget(
+      Directionality(
+        textDirection: TextDirection.ltr,
+        child: MediaQuery(
+          data: const MediaQueryData(),
+          child: PrimaryScrollController(
+            controller: scrollController,
+            child: RawScrollbar(
+              isAlwaysShown: true,
+              controller: scrollController,
+              child: const SingleChildScrollView(
+                child: SizedBox(width: 4000.0, height: 4000.0)
+              ),
+            ),
+          ),
+        ),
+      ),
+    );
+    await tester.pumpAndSettle();
+    expect(scrollController.offset, 0.0);
+    expect(
+      find.byType(RawScrollbar),
+      paints
+        ..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0))
+        ..rect(
+          rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 90.0),
+          color: const Color(0x66BCBCBC),
+        ),
+    );
+
+    // Drag the scrollbar just outside of the painted thumb with touch input.
+    // The hit test area is padded to meet the minimum interactive size.
+    const double scrollAmount = 10.0;
+    final TestGesture dragScrollbarGesture = await tester.startGesture(const Offset(790.0, 45.0));
+    await tester.pumpAndSettle();
+    await dragScrollbarGesture.moveBy(const Offset(0.0, scrollAmount));
+    await tester.pumpAndSettle();
+
+    // The scrollbar moved by scrollAmount, and the scrollOffset moved forward.
+    expect(scrollController.offset, greaterThan(0.0));
+    expect(
+      find.byType(RawScrollbar),
+      paints
+        ..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0))
+        ..rect(
+          rect: const Rect.fromLTRB(794.0, 10.0, 800.0, 100.0),
+          color: const Color(0x66BCBCBC),
+        ),
+    );
+
+    // Move back to reset.
+    await dragScrollbarGesture.moveBy(const Offset(0.0, -scrollAmount));
+    await tester.pumpAndSettle();
+    expect(scrollController.offset, 0.0);
+    expect(
+      find.byType(RawScrollbar),
+      paints
+        ..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0))
+        ..rect(
+          rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 90.0),
+          color: const Color(0x66BCBCBC),
+        ),
+    );
+
+    // The same should not be possible with a mouse since it is more precise,
+    // the padding it not necessary.
+    final TestGesture gesture = await tester.createGesture(kind: ui.PointerDeviceKind.mouse);
+    await gesture.addPointer();
+    addTearDown(gesture.removePointer);
+    await gesture.down(const Offset(790.0, 45.0));
+    await tester.pump();
+    await gesture.moveTo(const Offset(790.0, 55.0));
+    await gesture.up();
+    await tester.pumpAndSettle();
+    // The scrollbar/scrollable should not have moved.
+    expect(scrollController.offset, 0.0);
+    expect(
+      find.byType(RawScrollbar),
+      paints
+        ..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0))
+        ..rect(
+          rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 90.0),
+          color: const Color(0x66BCBCBC),
+        ),
+    );
+  });
 }