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 { ...@@ -69,6 +69,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
PointerDeviceKind? kind, PointerDeviceKind? kind,
this.dragStartBehavior = DragStartBehavior.start, this.dragStartBehavior = DragStartBehavior.start,
this.velocityTrackerBuilder = _defaultBuilder, this.velocityTrackerBuilder = _defaultBuilder,
this.supportedDevices = _kAllPointerDeviceKinds
}) : assert(dragStartBehavior != null), }) : assert(dragStartBehavior != null),
super(debugOwner: debugOwner, kind: kind); super(debugOwner: debugOwner, kind: kind);
...@@ -200,6 +201,11 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -200,6 +201,11 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
/// match the native behavior on that platform. /// match the native behavior on that platform.
GestureVelocityTrackerBuilder velocityTrackerBuilder; 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; _DragState _state = _DragState.ready;
late OffsetPair _initialPosition; late OffsetPair _initialPosition;
late OffsetPair _pendingDragOffset; late OffsetPair _pendingDragOffset;
...@@ -230,6 +236,9 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -230,6 +236,9 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
@override @override
bool isPointerAllowed(PointerEvent event) { bool isPointerAllowed(PointerEvent event) {
if (!supportedDevices.contains(event.kind)) {
return false;
}
if (_initialButtons == null) { if (_initialButtons == null) {
switch (event.buttons) { switch (event.buttons) {
case kPrimaryButton: case kPrimaryButton:
...@@ -508,7 +517,8 @@ class VerticalDragGestureRecognizer extends DragGestureRecognizer { ...@@ -508,7 +517,8 @@ class VerticalDragGestureRecognizer extends DragGestureRecognizer {
VerticalDragGestureRecognizer({ VerticalDragGestureRecognizer({
Object? debugOwner, Object? debugOwner,
PointerDeviceKind? kind, PointerDeviceKind? kind,
}) : super(debugOwner: debugOwner, kind: kind); Set<PointerDeviceKind> supportedDevices = _kAllPointerDeviceKinds,
}) : super(debugOwner: debugOwner, kind: kind, supportedDevices: supportedDevices);
@override @override
bool isFlingGesture(VelocityEstimate estimate, PointerDeviceKind kind) { bool isFlingGesture(VelocityEstimate estimate, PointerDeviceKind kind) {
...@@ -532,6 +542,10 @@ class VerticalDragGestureRecognizer extends DragGestureRecognizer { ...@@ -532,6 +542,10 @@ class VerticalDragGestureRecognizer extends DragGestureRecognizer {
String get debugDescription => 'vertical drag'; String get debugDescription => 'vertical drag';
} }
const Set<PointerDeviceKind> _kAllPointerDeviceKinds = <PointerDeviceKind>{
...PointerDeviceKind.values,
};
/// Recognizes movement in the horizontal direction. /// Recognizes movement in the horizontal direction.
/// ///
/// Used for horizontal scrolling. /// Used for horizontal scrolling.
...@@ -549,7 +563,8 @@ class HorizontalDragGestureRecognizer extends DragGestureRecognizer { ...@@ -549,7 +563,8 @@ class HorizontalDragGestureRecognizer extends DragGestureRecognizer {
HorizontalDragGestureRecognizer({ HorizontalDragGestureRecognizer({
Object? debugOwner, Object? debugOwner,
PointerDeviceKind? kind, PointerDeviceKind? kind,
}) : super(debugOwner: debugOwner, kind: kind); Set<PointerDeviceKind> supportedDevices = _kAllPointerDeviceKinds,
}) : super(debugOwner: debugOwner, kind: kind, supportedDevices: supportedDevices);
@override @override
bool isFlingGesture(VelocityEstimate estimate, PointerDeviceKind kind) { bool isFlingGesture(VelocityEstimate estimate, PointerDeviceKind kind) {
......
...@@ -14,6 +14,13 @@ import 'scrollbar.dart'; ...@@ -14,6 +14,13 @@ import 'scrollbar.dart';
const Color _kDefaultGlowColor = Color(0xFFFFFFFF); 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. /// Describes how [Scrollable] widgets should behave.
/// ///
/// {@template flutter.widgets.scrollBehavior} /// {@template flutter.widgets.scrollBehavior}
...@@ -52,6 +59,7 @@ class ScrollBehavior { ...@@ -52,6 +59,7 @@ class ScrollBehavior {
ScrollBehavior copyWith({ ScrollBehavior copyWith({
bool scrollbars = true, bool scrollbars = true,
bool overscroll = true, bool overscroll = true,
Set<PointerDeviceKind>? dragDevices,
ScrollPhysics? physics, ScrollPhysics? physics,
TargetPlatform? platform, TargetPlatform? platform,
}) { }) {
...@@ -61,6 +69,7 @@ class ScrollBehavior { ...@@ -61,6 +69,7 @@ class ScrollBehavior {
overscrollIndicator: overscroll, overscrollIndicator: overscroll,
physics: physics, physics: physics,
platform: platform, platform: platform,
dragDevices: dragDevices,
); );
} }
...@@ -69,6 +78,14 @@ class ScrollBehavior { ...@@ -69,6 +78,14 @@ class ScrollBehavior {
/// Defaults to the current platform. /// Defaults to the current platform.
TargetPlatform getPlatform(BuildContext context) => defaultTargetPlatform; 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]. /// Wraps the given widget, which scrolls in the given [AxisDirection].
/// ///
/// For example, on Android, this method wraps the given widget with a /// For example, on Android, this method wraps the given widget with a
...@@ -200,13 +217,18 @@ class _WrappedScrollBehavior implements ScrollBehavior { ...@@ -200,13 +217,18 @@ class _WrappedScrollBehavior implements ScrollBehavior {
this.overscrollIndicator = true, this.overscrollIndicator = true,
this.physics, this.physics,
this.platform, this.platform,
}); Set<PointerDeviceKind>? dragDevices,
}) : _dragDevices = dragDevices;
final ScrollBehavior delegate; final ScrollBehavior delegate;
final bool scrollbar; final bool scrollbar;
final bool overscrollIndicator; final bool overscrollIndicator;
final ScrollPhysics? physics; final ScrollPhysics? physics;
final TargetPlatform? platform; final TargetPlatform? platform;
final Set<PointerDeviceKind>? _dragDevices;
@override
Set<PointerDeviceKind> get dragDevices => _dragDevices ?? delegate.dragDevices;
@override @override
Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) { Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) {
...@@ -233,12 +255,14 @@ class _WrappedScrollBehavior implements ScrollBehavior { ...@@ -233,12 +255,14 @@ class _WrappedScrollBehavior implements ScrollBehavior {
bool overscroll = true, bool overscroll = true,
ScrollPhysics? physics, ScrollPhysics? physics,
TargetPlatform? platform, TargetPlatform? platform,
Set<PointerDeviceKind>? dragDevices,
}) { }) {
return delegate.copyWith( return delegate.copyWith(
scrollbars: scrollbars, scrollbars: scrollbars,
overscroll: overscroll, overscroll: overscroll,
physics: physics, physics: physics,
platform: platform, platform: platform,
dragDevices: dragDevices,
); );
} }
...@@ -259,6 +283,7 @@ class _WrappedScrollBehavior implements ScrollBehavior { ...@@ -259,6 +283,7 @@ class _WrappedScrollBehavior implements ScrollBehavior {
|| oldDelegate.overscrollIndicator != overscrollIndicator || oldDelegate.overscrollIndicator != overscrollIndicator
|| oldDelegate.physics != physics || oldDelegate.physics != physics
|| oldDelegate.platform != platform || oldDelegate.platform != platform
|| setEquals<PointerDeviceKind>(oldDelegate.dragDevices, dragDevices)
|| delegate.shouldNotify(oldDelegate.delegate); || delegate.shouldNotify(oldDelegate.delegate);
} }
......
...@@ -565,7 +565,8 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R ...@@ -565,7 +565,8 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
..minFlingVelocity = _physics?.minFlingVelocity ..minFlingVelocity = _physics?.minFlingVelocity
..maxFlingVelocity = _physics?.maxFlingVelocity ..maxFlingVelocity = _physics?.maxFlingVelocity
..velocityTrackerBuilder = _configuration.velocityTrackerBuilder(context) ..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 ...@@ -585,7 +586,8 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
..minFlingVelocity = _physics?.minFlingVelocity ..minFlingVelocity = _physics?.minFlingVelocity
..maxFlingVelocity = _physics?.maxFlingVelocity ..maxFlingVelocity = _physics?.maxFlingVelocity
..velocityTrackerBuilder = _configuration.velocityTrackerBuilder(context) ..velocityTrackerBuilder = _configuration.velocityTrackerBuilder(context)
..dragStartBehavior = widget.dragStartBehavior; ..dragStartBehavior = widget.dragStartBehavior
..supportedDevices = _configuration.dragDevices;
}, },
), ),
}; };
......
...@@ -181,6 +181,106 @@ void main() { ...@@ -181,6 +181,106 @@ void main() {
didEndDrag = false; 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) { testGesture('Should report original timestamps', (GestureTester tester) {
final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer() ..dragStartBehavior = DragStartBehavior.down; final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer() ..dragStartBehavior = DragStartBehavior.down;
addTearDown(drag.dispose); addTearDown(drag.dispose);
......
...@@ -17,9 +17,13 @@ Future<void> pumpTest( ...@@ -17,9 +17,13 @@ Future<void> pumpTest(
bool scrollable = true, bool scrollable = true,
bool reverse = false, bool reverse = false,
ScrollController? controller, ScrollController? controller,
bool enableMouseDrag = true,
}) async { }) async {
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
scrollBehavior: const NoScrollbarBehavior(), scrollBehavior: const NoScrollbarBehavior().copyWith(dragDevices: enableMouseDrag
? <ui.PointerDeviceKind>{...ui.PointerDeviceKind.values}
: null,
),
theme: ThemeData( theme: ThemeData(
platform: platform, platform: platform,
), ),
...@@ -1269,6 +1273,46 @@ void main() { ...@@ -1269,6 +1273,46 @@ void main() {
expect(tester.takeException(), null); 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 // ignore: must_be_immutable
......
...@@ -530,6 +530,7 @@ abstract class WidgetController { ...@@ -530,6 +530,7 @@ abstract class WidgetController {
double touchSlopX = kDragSlopDefault, double touchSlopX = kDragSlopDefault,
double touchSlopY = kDragSlopDefault, double touchSlopY = kDragSlopDefault,
bool warnIfMissed = true, bool warnIfMissed = true,
PointerDeviceKind kind = PointerDeviceKind.touch,
}) { }) {
return dragFrom( return dragFrom(
getCenter(finder, warnIfMissed: warnIfMissed, callee: 'drag'), getCenter(finder, warnIfMissed: warnIfMissed, callee: 'drag'),
...@@ -538,6 +539,7 @@ abstract class WidgetController { ...@@ -538,6 +539,7 @@ abstract class WidgetController {
buttons: buttons, buttons: buttons,
touchSlopX: touchSlopX, touchSlopX: touchSlopX,
touchSlopY: touchSlopY, touchSlopY: touchSlopY,
kind: kind,
); );
} }
...@@ -559,10 +561,11 @@ abstract class WidgetController { ...@@ -559,10 +561,11 @@ abstract class WidgetController {
int buttons = kPrimaryButton, int buttons = kPrimaryButton,
double touchSlopX = kDragSlopDefault, double touchSlopX = kDragSlopDefault,
double touchSlopY = kDragSlopDefault, double touchSlopY = kDragSlopDefault,
PointerDeviceKind kind = PointerDeviceKind.touch,
}) { }) {
assert(kDragSlopDefault > kTouchSlop); assert(kDragSlopDefault > kTouchSlop);
return TestAsyncUtils.guard<void>(() async { 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); assert(gesture != null);
final double xSign = offset.dx.sign; 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