Unverified Commit fff8ecfb authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[flutter] reject mouse drags by default in scrollables (#81569)

parent 7b3ce8c0
......@@ -69,6 +69,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
PointerDeviceKind? kind,
this.dragStartBehavior = DragStartBehavior.start,
this.velocityTrackerBuilder = _defaultBuilder,
this.supportedDevices = _kAllPointerDeviceKinds
}) : assert(dragStartBehavior != null),
super(debugOwner: debugOwner, kind: kind);
......@@ -200,6 +201,11 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
/// match the native behavior on that platform.
GestureVelocityTrackerBuilder velocityTrackerBuilder;
/// The device types that this gesture recognizer will accept drags from.
///
/// If not specified, defaults to all pointer kinds.
Set<PointerDeviceKind> supportedDevices;
_DragState _state = _DragState.ready;
late OffsetPair _initialPosition;
late OffsetPair _pendingDragOffset;
......@@ -230,6 +236,9 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
@override
bool isPointerAllowed(PointerEvent event) {
if (!supportedDevices.contains(event.kind)) {
return false;
}
if (_initialButtons == null) {
switch (event.buttons) {
case kPrimaryButton:
......@@ -508,7 +517,8 @@ class VerticalDragGestureRecognizer extends DragGestureRecognizer {
VerticalDragGestureRecognizer({
Object? debugOwner,
PointerDeviceKind? kind,
}) : super(debugOwner: debugOwner, kind: kind);
Set<PointerDeviceKind> supportedDevices = _kAllPointerDeviceKinds,
}) : super(debugOwner: debugOwner, kind: kind, supportedDevices: supportedDevices);
@override
bool isFlingGesture(VelocityEstimate estimate, PointerDeviceKind kind) {
......@@ -532,6 +542,10 @@ class VerticalDragGestureRecognizer extends DragGestureRecognizer {
String get debugDescription => 'vertical drag';
}
const Set<PointerDeviceKind> _kAllPointerDeviceKinds = <PointerDeviceKind>{
...PointerDeviceKind.values,
};
/// Recognizes movement in the horizontal direction.
///
/// Used for horizontal scrolling.
......@@ -549,7 +563,8 @@ class HorizontalDragGestureRecognizer extends DragGestureRecognizer {
HorizontalDragGestureRecognizer({
Object? debugOwner,
PointerDeviceKind? kind,
}) : super(debugOwner: debugOwner, kind: kind);
Set<PointerDeviceKind> supportedDevices = _kAllPointerDeviceKinds,
}) : super(debugOwner: debugOwner, kind: kind, supportedDevices: supportedDevices);
@override
bool isFlingGesture(VelocityEstimate estimate, PointerDeviceKind kind) {
......
......@@ -14,6 +14,13 @@ import 'scrollbar.dart';
const Color _kDefaultGlowColor = Color(0xFFFFFFFF);
/// Device types that scrollables should accept drag gestures from by default.
const Set<PointerDeviceKind> _kTouchLikeDeviceTypes = <PointerDeviceKind>{
PointerDeviceKind.touch,
PointerDeviceKind.stylus,
PointerDeviceKind.invertedStylus,
};
/// Describes how [Scrollable] widgets should behave.
///
/// {@template flutter.widgets.scrollBehavior}
......@@ -52,6 +59,7 @@ class ScrollBehavior {
ScrollBehavior copyWith({
bool scrollbars = true,
bool overscroll = true,
Set<PointerDeviceKind>? dragDevices,
ScrollPhysics? physics,
TargetPlatform? platform,
}) {
......@@ -61,6 +69,7 @@ class ScrollBehavior {
overscrollIndicator: overscroll,
physics: physics,
platform: platform,
dragDevices: dragDevices,
);
}
......@@ -69,6 +78,14 @@ class ScrollBehavior {
/// Defaults to the current platform.
TargetPlatform getPlatform(BuildContext context) => defaultTargetPlatform;
/// The device kinds that the scrollable will accept drag gestures from.
///
/// By default only [PointerDeviceKind.touch], [PointerDeviceKind.stylus], and
/// [PointerDeviceKind.invertedStylus] are configured to create drag gestures.
/// Enabling this for [PointerDeviceKind.mouse] will make it difficult or
/// impossible to select text in scrollable containers and is not recommended.
Set<PointerDeviceKind> get dragDevices => _kTouchLikeDeviceTypes;
/// Wraps the given widget, which scrolls in the given [AxisDirection].
///
/// For example, on Android, this method wraps the given widget with a
......@@ -200,13 +217,18 @@ class _WrappedScrollBehavior implements ScrollBehavior {
this.overscrollIndicator = true,
this.physics,
this.platform,
});
Set<PointerDeviceKind>? dragDevices,
}) : _dragDevices = dragDevices;
final ScrollBehavior delegate;
final bool scrollbar;
final bool overscrollIndicator;
final ScrollPhysics? physics;
final TargetPlatform? platform;
final Set<PointerDeviceKind>? _dragDevices;
@override
Set<PointerDeviceKind> get dragDevices => _dragDevices ?? delegate.dragDevices;
@override
Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) {
......@@ -233,12 +255,14 @@ class _WrappedScrollBehavior implements ScrollBehavior {
bool overscroll = true,
ScrollPhysics? physics,
TargetPlatform? platform,
Set<PointerDeviceKind>? dragDevices,
}) {
return delegate.copyWith(
scrollbars: scrollbars,
overscroll: overscroll,
physics: physics,
platform: platform,
dragDevices: dragDevices,
);
}
......@@ -259,6 +283,7 @@ class _WrappedScrollBehavior implements ScrollBehavior {
|| oldDelegate.overscrollIndicator != overscrollIndicator
|| oldDelegate.physics != physics
|| oldDelegate.platform != platform
|| setEquals<PointerDeviceKind>(oldDelegate.dragDevices, dragDevices)
|| delegate.shouldNotify(oldDelegate.delegate);
}
......
......@@ -565,7 +565,8 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
..minFlingVelocity = _physics?.minFlingVelocity
..maxFlingVelocity = _physics?.maxFlingVelocity
..velocityTrackerBuilder = _configuration.velocityTrackerBuilder(context)
..dragStartBehavior = widget.dragStartBehavior;
..dragStartBehavior = widget.dragStartBehavior
..supportedDevices = _configuration.dragDevices;
},
),
};
......@@ -585,7 +586,8 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
..minFlingVelocity = _physics?.minFlingVelocity
..maxFlingVelocity = _physics?.maxFlingVelocity
..velocityTrackerBuilder = _configuration.velocityTrackerBuilder(context)
..dragStartBehavior = widget.dragStartBehavior;
..dragStartBehavior = widget.dragStartBehavior
..supportedDevices = _configuration.dragDevices;
},
),
};
......
......@@ -181,6 +181,106 @@ void main() {
didEndDrag = false;
});
testGesture('Should reject mouse drag when configured to ignore mouse pointers - Horizontal', (GestureTester tester) {
final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer(supportedDevices: <PointerDeviceKind>{
PointerDeviceKind.touch,
}) ..dragStartBehavior = DragStartBehavior.down;
addTearDown(drag.dispose);
bool didStartDrag = false;
drag.onStart = (_) {
didStartDrag = true;
};
double? updatedDelta;
drag.onUpdate = (DragUpdateDetails details) {
updatedDelta = details.primaryDelta;
};
bool didEndDrag = false;
drag.onEnd = (DragEndDetails details) {
didEndDrag = true;
};
final TestPointer pointer = TestPointer(5, PointerDeviceKind.mouse);
final PointerDownEvent down = pointer.down(const Offset(10.0, 10.0));
drag.addPointer(down);
tester.closeArena(5);
expect(didStartDrag, isFalse);
expect(updatedDelta, isNull);
expect(didEndDrag, isFalse);
tester.route(down);
expect(didStartDrag, isFalse);
expect(updatedDelta, isNull);
expect(didEndDrag, isFalse);
tester.route(pointer.move(const Offset(20.0, 25.0)));
expect(didStartDrag, isFalse);
expect(updatedDelta, isNull);
expect(didEndDrag, isFalse);
tester.route(pointer.move(const Offset(20.0, 25.0)));
expect(didStartDrag, isFalse);
expect(updatedDelta, isNull);
expect(didEndDrag, isFalse);
tester.route(pointer.up());
expect(didStartDrag, isFalse);
expect(updatedDelta, isNull);
expect(didEndDrag, isFalse);
});
testGesture('Should reject mouse drag when configured to ignore mouse pointers - Vertical', (GestureTester tester) {
final VerticalDragGestureRecognizer drag = VerticalDragGestureRecognizer(supportedDevices: <PointerDeviceKind>{
PointerDeviceKind.touch,
})..dragStartBehavior = DragStartBehavior.down;
addTearDown(drag.dispose);
bool didStartDrag = false;
drag.onStart = (_) {
didStartDrag = true;
};
double? updatedDelta;
drag.onUpdate = (DragUpdateDetails details) {
updatedDelta = details.primaryDelta;
};
bool didEndDrag = false;
drag.onEnd = (DragEndDetails details) {
didEndDrag = true;
};
final TestPointer pointer = TestPointer(5, PointerDeviceKind.mouse);
final PointerDownEvent down = pointer.down(const Offset(10.0, 10.0));
drag.addPointer(down);
tester.closeArena(5);
expect(didStartDrag, isFalse);
expect(updatedDelta, isNull);
expect(didEndDrag, isFalse);
tester.route(down);
expect(didStartDrag, isFalse);
expect(updatedDelta, isNull);
expect(didEndDrag, isFalse);
tester.route(pointer.move(const Offset(25.0, 20.0)));
expect(didStartDrag, isFalse);
expect(updatedDelta, isNull);
expect(didEndDrag, isFalse);
tester.route(pointer.move(const Offset(25.0, 20.0)));
expect(didStartDrag, isFalse);
expect(updatedDelta, isNull);
expect(didEndDrag, isFalse);
tester.route(pointer.up());
expect(didStartDrag, isFalse);
expect(updatedDelta, isNull);
expect(didEndDrag, isFalse);
});
testGesture('Should report original timestamps', (GestureTester tester) {
final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer() ..dragStartBehavior = DragStartBehavior.down;
addTearDown(drag.dispose);
......
......@@ -17,9 +17,13 @@ Future<void> pumpTest(
bool scrollable = true,
bool reverse = false,
ScrollController? controller,
bool enableMouseDrag = true,
}) async {
await tester.pumpWidget(MaterialApp(
scrollBehavior: const NoScrollbarBehavior(),
scrollBehavior: const NoScrollbarBehavior().copyWith(dragDevices: enableMouseDrag
? <ui.PointerDeviceKind>{...ui.PointerDeviceKind.values}
: null,
),
theme: ThemeData(
platform: platform,
),
......@@ -1269,6 +1273,46 @@ void main() {
expect(tester.takeException(), null);
});
testWidgets('Does not scroll with mouse pointer drag when behavior is configured to ignore them', (WidgetTester tester) async {
await pumpTest(tester, debugDefaultTargetPlatformOverride, enableMouseDrag: false);
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(Scrollable), warnIfMissed: true), kind: ui.PointerDeviceKind.mouse);
await gesture.moveBy(const Offset(0.0, -200));
await tester.pump();
await tester.pumpAndSettle();
expect(getScrollOffset(tester), 0.0);
await gesture.moveBy(const Offset(0.0, 200));
await tester.pump();
await tester.pumpAndSettle();
expect(getScrollOffset(tester), 0.0);
await gesture.removePointer();
await tester.pump();
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS, TargetPlatform.android }));
testWidgets('Does scroll with mouse pointer drag when behavior is not configured to ignore them', (WidgetTester tester) async {
await pumpTest(tester, debugDefaultTargetPlatformOverride, enableMouseDrag: true);
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(Scrollable), warnIfMissed: true), kind: ui.PointerDeviceKind.mouse);
await gesture.moveBy(const Offset(0.0, -200));
await tester.pump();
await tester.pumpAndSettle();
expect(getScrollOffset(tester), 200.0);
await gesture.moveBy(const Offset(0.0, 200));
await tester.pump();
await tester.pumpAndSettle();
expect(getScrollOffset(tester), 0.0);
await gesture.removePointer();
await tester.pump();
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS, TargetPlatform.android }));
}
// ignore: must_be_immutable
......
......@@ -530,6 +530,7 @@ abstract class WidgetController {
double touchSlopX = kDragSlopDefault,
double touchSlopY = kDragSlopDefault,
bool warnIfMissed = true,
PointerDeviceKind kind = PointerDeviceKind.touch,
}) {
return dragFrom(
getCenter(finder, warnIfMissed: warnIfMissed, callee: 'drag'),
......@@ -538,6 +539,7 @@ abstract class WidgetController {
buttons: buttons,
touchSlopX: touchSlopX,
touchSlopY: touchSlopY,
kind: kind,
);
}
......@@ -559,10 +561,11 @@ abstract class WidgetController {
int buttons = kPrimaryButton,
double touchSlopX = kDragSlopDefault,
double touchSlopY = kDragSlopDefault,
PointerDeviceKind kind = PointerDeviceKind.touch,
}) {
assert(kDragSlopDefault > kTouchSlop);
return TestAsyncUtils.guard<void>(() async {
final TestGesture gesture = await startGesture(startLocation, pointer: pointer, buttons: buttons);
final TestGesture gesture = await startGesture(startLocation, pointer: pointer, buttons: buttons, kind: kind);
assert(gesture != null);
final double xSign = offset.dx.sign;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment