diff --git a/packages/flutter/lib/src/material/reorderable_list.dart b/packages/flutter/lib/src/material/reorderable_list.dart index 49a62f6142..0ec5f8b9f2 100644 --- a/packages/flutter/lib/src/material/reorderable_list.dart +++ b/packages/flutter/lib/src/material/reorderable_list.dart @@ -103,6 +103,7 @@ class ReorderableListView extends StatefulWidget { this.clipBehavior = Clip.hardEdge, this.autoScrollerVelocityScalar, this.dragBoundaryProvider, + this.mouseCursor, }) : assert( (itemExtent == null && prototypeItem == null) || (itemExtent == null && itemExtentBuilder == null) || @@ -173,6 +174,7 @@ class ReorderableListView extends StatefulWidget { this.clipBehavior = Clip.hardEdge, this.autoScrollerVelocityScalar, this.dragBoundaryProvider, + this.mouseCursor, }) : assert(itemCount >= 0), assert( (itemExtent == null && prototypeItem == null) || @@ -297,11 +299,25 @@ class ReorderableListView extends StatefulWidget { /// {@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 createState() => _ReorderableListViewState(); } class _ReorderableListViewState extends State { + final ValueNotifier _dragging = ValueNotifier(false); + Widget _itemBuilder(BuildContext context, int index) { final Widget item = widget.itemBuilder(context, index); assert(() { @@ -318,6 +334,21 @@ class _ReorderableListViewState extends State { 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( + widget.mouseCursor ?? + const WidgetStateMouseCursor.fromMap({ + WidgetState.dragged: SystemMouseCursors.grabbing, + WidgetState.any: SystemMouseCursors.grab, + }), + {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 @@ class _ReorderableListViewState extends State { 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 @@ class _ReorderableListViewState extends State { end: 8, child: Align( alignment: AlignmentDirectional.centerEnd, - child: ReorderableDragStartListener( - index: index, - child: const Icon(Icons.drag_handle), - ), + child: ReorderableDragStartListener(index: index, child: dragHandle), ), ), ], @@ -383,6 +408,12 @@ class _ReorderableListViewState extends State { ); } + @override + void dispose() { + _dragging.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { assert(debugCheckHasMaterialLocalizations(context)); @@ -440,8 +471,14 @@ class _ReorderableListViewState extends State { 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 080b3baaf4..4193071e20 100644 --- a/packages/flutter/test/material/reorderable_list_test.dart +++ b/packages/flutter/test/material/reorderable_list_test.dart @@ -2318,6 +2318,101 @@ void main() { 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(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({ + WidgetState.dragged: SystemMouseCursors.copy, + WidgetState.any: SystemMouseCursors.resizeColumn, + }), + itemBuilder: (BuildContext context, int index) { + return ReorderableDragStartListener( + key: ValueKey(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 longPressDrag(WidgetTester tester, Offset start, Offset end) async {