Add `mouseCursor` parameter to `ReorderableListView` (#160246)

Part of https://github.com/flutter/flutter/issues/58192#issue-626789189


<table>
  <tr>
    <th>Defautl</th>
    <th>Customized</th>
  </tr>
  <tr>
    <td>



https://github.com/user-attachments/assets/91819d7e-472b-481d-84ff-bec0d812ab53



</td>
    <td>


https://github.com/user-attachments/assets/61a6841f-1845-4444-a03a-8870f0b67969




</td>
  </tr>
</table>


## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
diff --git a/packages/flutter/lib/src/material/reorderable_list.dart b/packages/flutter/lib/src/material/reorderable_list.dart
index 49a62f6..0ec5f8b 100644
--- a/packages/flutter/lib/src/material/reorderable_list.dart
+++ b/packages/flutter/lib/src/material/reorderable_list.dart
@@ -103,6 +103,7 @@
     this.clipBehavior = Clip.hardEdge,
     this.autoScrollerVelocityScalar,
     this.dragBoundaryProvider,
+    this.mouseCursor,
   }) : assert(
          (itemExtent == null && prototypeItem == null) ||
              (itemExtent == null && itemExtentBuilder == null) ||
@@ -173,6 +174,7 @@
     this.clipBehavior = Clip.hardEdge,
     this.autoScrollerVelocityScalar,
     this.dragBoundaryProvider,
+    this.mouseCursor,
   }) : assert(itemCount >= 0),
        assert(
          (itemExtent == null && prototypeItem == null) ||
@@ -297,11 +299,25 @@
   /// {@macro flutter.widgets.reorderable_list.dragBoundaryProvider}
   final ReorderDragBoundaryProvider? dragBoundaryProvider;
 
+  /// The cursor for a mouse pointer when it enters or is hovering over the drag
+  /// handle.
+  ///
+  /// If [mouseCursor] is a [WidgetStateMouseCursor],
+  /// [WidgetStateProperty.resolve] is used for the following [WidgetState]s:
+  ///
+  ///  * [WidgetState.dragged].
+  ///
+  /// If this property is null, [SystemMouseCursors.grab] will be used when
+  ///  hovering, and [SystemMouseCursors.grabbing] when dragging.
+  final MouseCursor? mouseCursor;
+
   @override
   State<ReorderableListView> createState() => _ReorderableListViewState();
 }
 
 class _ReorderableListViewState extends State<ReorderableListView> {
+  final ValueNotifier<bool> _dragging = ValueNotifier<bool>(false);
+
   Widget _itemBuilder(BuildContext context, int index) {
     final Widget item = widget.itemBuilder(context, index);
     assert(() {
@@ -318,6 +334,21 @@
         case TargetPlatform.linux:
         case TargetPlatform.windows:
         case TargetPlatform.macOS:
+          final ListenableBuilder dragHandle = ListenableBuilder(
+            listenable: _dragging,
+            builder: (BuildContext context, Widget? child) {
+              final MouseCursor effectiveMouseCursor = WidgetStateProperty.resolveAs<MouseCursor>(
+                widget.mouseCursor ??
+                    const WidgetStateMouseCursor.fromMap(<WidgetStatesConstraint, MouseCursor>{
+                      WidgetState.dragged: SystemMouseCursors.grabbing,
+                      WidgetState.any: SystemMouseCursors.grab,
+                    }),
+                <WidgetState>{if (_dragging.value) WidgetState.dragged},
+              );
+              return MouseRegion(cursor: effectiveMouseCursor, child: child);
+            },
+            child: const Icon(Icons.drag_handle),
+          );
           switch (widget.scrollDirection) {
             case Axis.horizontal:
               return Stack(
@@ -331,10 +362,7 @@
                     bottom: 8,
                     child: Align(
                       alignment: AlignmentDirectional.bottomCenter,
-                      child: ReorderableDragStartListener(
-                        index: index,
-                        child: const Icon(Icons.drag_handle),
-                      ),
+                      child: ReorderableDragStartListener(index: index, child: dragHandle),
                     ),
                   ),
                 ],
@@ -351,10 +379,7 @@
                     end: 8,
                     child: Align(
                       alignment: AlignmentDirectional.centerEnd,
-                      child: ReorderableDragStartListener(
-                        index: index,
-                        child: const Icon(Icons.drag_handle),
-                      ),
+                      child: ReorderableDragStartListener(index: index, child: dragHandle),
                     ),
                   ),
                 ],
@@ -384,6 +409,12 @@
   }
 
   @override
+  void dispose() {
+    _dragging.dispose();
+    super.dispose();
+  }
+
+  @override
   Widget build(BuildContext context) {
     assert(debugCheckHasMaterialLocalizations(context));
     assert(debugCheckHasOverlay(context));
@@ -440,8 +471,14 @@
             prototypeItem: widget.prototypeItem,
             itemCount: widget.itemCount,
             onReorder: widget.onReorder,
-            onReorderStart: widget.onReorderStart,
-            onReorderEnd: widget.onReorderEnd,
+            onReorderStart: (int index) {
+              _dragging.value = true;
+              widget.onReorderStart?.call(index);
+            },
+            onReorderEnd: (int index) {
+              _dragging.value = false;
+              widget.onReorderEnd?.call(index);
+            },
             proxyDecorator: widget.proxyDecorator ?? _proxyDecorator,
             autoScrollerVelocityScalar: widget.autoScrollerVelocityScalar,
             dragBoundaryProvider: widget.dragBoundaryProvider,
diff --git a/packages/flutter/test/material/reorderable_list_test.dart b/packages/flutter/test/material/reorderable_list_test.dart
index 080b3ba..4193071 100644
--- a/packages/flutter/test/material/reorderable_list_test.dart
+++ b/packages/flutter/test/material/reorderable_list_test.dart
@@ -2318,6 +2318,101 @@
     await drag.up();
     await tester.pumpAndSettle();
   });
+
+  testWidgets('Mouse cursor behavior on the drag handle', (WidgetTester tester) async {
+    await tester.pumpWidget(
+      MaterialApp(
+        home: Scaffold(
+          body: ReorderableListView.builder(
+            itemBuilder: (BuildContext context, int index) {
+              return ReorderableDragStartListener(
+                key: ValueKey<int>(index),
+                index: index,
+                child: Text('$index'),
+              );
+            },
+            itemCount: 5,
+            onReorder: (int fromIndex, int toIndex) {},
+          ),
+        ),
+      ),
+    );
+
+    final TestGesture gesture = await tester.createGesture(
+      kind: PointerDeviceKind.mouse,
+      pointer: 1,
+    );
+    await gesture.addPointer(location: tester.getCenter(find.byIcon(Icons.drag_handle).first));
+    await tester.pump();
+    expect(
+      RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
+      SystemMouseCursors.grab,
+    );
+    await gesture.down(tester.getCenter(find.byIcon(Icons.drag_handle).first));
+    await tester.pump(kLongPressTimeout);
+    expect(
+      RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
+      SystemMouseCursors.grabbing,
+    );
+    await gesture.up();
+    await tester.pumpAndSettle();
+    expect(
+      RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
+      SystemMouseCursors.grab,
+    );
+  }, variant: TargetPlatformVariant.desktop());
+
+  testWidgets(
+    'Mouse cursor behavior on the drag handle can be provided',
+    (WidgetTester tester) async {
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Scaffold(
+            body: ReorderableListView.builder(
+              mouseCursor:
+                  const WidgetStateMouseCursor.fromMap(<WidgetStatesConstraint, MouseCursor>{
+                    WidgetState.dragged: SystemMouseCursors.copy,
+                    WidgetState.any: SystemMouseCursors.resizeColumn,
+                  }),
+              itemBuilder: (BuildContext context, int index) {
+                return ReorderableDragStartListener(
+                  key: ValueKey<int>(index),
+                  index: index,
+                  child: Text('$index'),
+                );
+              },
+              itemCount: 5,
+              onReorder: (int fromIndex, int toIndex) {},
+            ),
+          ),
+        ),
+      );
+
+      final TestGesture gesture = await tester.createGesture(
+        kind: PointerDeviceKind.mouse,
+        pointer: 1,
+      );
+      await gesture.addPointer(location: tester.getCenter(find.byIcon(Icons.drag_handle).first));
+      await tester.pump();
+      expect(
+        RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
+        SystemMouseCursors.resizeColumn,
+      );
+      await gesture.down(tester.getCenter(find.byIcon(Icons.drag_handle).first));
+      await tester.pump(kLongPressTimeout);
+      expect(
+        RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
+        SystemMouseCursors.copy,
+      );
+      await gesture.up();
+      await tester.pumpAndSettle();
+      expect(
+        RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
+        SystemMouseCursors.resizeColumn,
+      );
+    },
+    variant: TargetPlatformVariant.desktop(),
+  );
 }
 
 Future<void> longPressDrag(WidgetTester tester, Offset start, Offset end) async {