Unverified Commit e69ea6de authored by Kate Lovett's avatar Kate Lovett Committed by GitHub

Support flipping mouse scrolling axes through modifier keys (#115610)

* Maybe maybe

* Nit

* One more nit

* ++

* Fix test

* REview feedback

* Add comment about ios

* ++

* Doc nit

* Handle trackpads

* Review feedback
parent 54405bfa
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart' show LogicalKeyboardKey;
import 'framework.dart'; import 'framework.dart';
import 'overscroll_indicator.dart'; import 'overscroll_indicator.dart';
...@@ -100,6 +101,7 @@ class ScrollBehavior { ...@@ -100,6 +101,7 @@ class ScrollBehavior {
bool? scrollbars, bool? scrollbars,
bool? overscroll, bool? overscroll,
Set<PointerDeviceKind>? dragDevices, Set<PointerDeviceKind>? dragDevices,
Set<LogicalKeyboardKey>? pointerAxisModifiers,
ScrollPhysics? physics, ScrollPhysics? physics,
TargetPlatform? platform, TargetPlatform? platform,
@Deprecated( @Deprecated(
...@@ -112,9 +114,10 @@ class ScrollBehavior { ...@@ -112,9 +114,10 @@ class ScrollBehavior {
delegate: this, delegate: this,
scrollbars: scrollbars ?? true, scrollbars: scrollbars ?? true,
overscroll: overscroll ?? true, overscroll: overscroll ?? true,
dragDevices: dragDevices,
pointerAxisModifiers: pointerAxisModifiers,
physics: physics, physics: physics,
platform: platform, platform: platform,
dragDevices: dragDevices,
androidOverscrollIndicator: androidOverscrollIndicator androidOverscrollIndicator: androidOverscrollIndicator
); );
} }
...@@ -132,6 +135,25 @@ class ScrollBehavior { ...@@ -132,6 +135,25 @@ class ScrollBehavior {
/// impossible to select text in scrollable containers and is not recommended. /// impossible to select text in scrollable containers and is not recommended.
Set<PointerDeviceKind> get dragDevices => _kTouchLikeDeviceTypes; Set<PointerDeviceKind> get dragDevices => _kTouchLikeDeviceTypes;
/// A set of [LogicalKeyboardKey]s that, when any or all are pressed in
/// combination with a [PointerDeviceKind.mouse] pointer scroll event, will
/// flip the axes of the scroll input.
///
/// This will for example, result in the input of a vertical mouse wheel, to
/// move the [ScrollPosition] of a [ScrollView] with an [Axis.horizontal]
/// scroll direction.
///
/// If other keys exclusive of this set are pressed during a scroll event, in
/// conjunction with keys from this set, the scroll input will still be
/// flipped.
///
/// Defaults to [LogicalKeyboardKey.shiftLeft],
/// [LogicalKeyboardKey.shiftRight].
Set<LogicalKeyboardKey> get pointerAxisModifiers => <LogicalKeyboardKey>{
LogicalKeyboardKey.shiftLeft,
LogicalKeyboardKey.shiftRight,
};
/// Applies a [RawScrollbar] to the child widget on desktop platforms. /// Applies a [RawScrollbar] to the child widget on desktop platforms.
Widget buildScrollbar(BuildContext context, Widget child, ScrollableDetails details) { Widget buildScrollbar(BuildContext context, Widget child, ScrollableDetails details) {
// When modifying this function, consider modifying the implementation in // When modifying this function, consider modifying the implementation in
...@@ -261,12 +283,14 @@ class _WrappedScrollBehavior implements ScrollBehavior { ...@@ -261,12 +283,14 @@ class _WrappedScrollBehavior implements ScrollBehavior {
required this.delegate, required this.delegate,
this.scrollbars = true, this.scrollbars = true,
this.overscroll = true, this.overscroll = true,
Set<PointerDeviceKind>? dragDevices,
Set<LogicalKeyboardKey>? pointerAxisModifiers,
this.physics, this.physics,
this.platform, this.platform,
Set<PointerDeviceKind>? dragDevices,
AndroidOverscrollIndicator? androidOverscrollIndicator, AndroidOverscrollIndicator? androidOverscrollIndicator,
}) : _androidOverscrollIndicator = androidOverscrollIndicator, }) : _androidOverscrollIndicator = androidOverscrollIndicator,
_dragDevices = dragDevices; _dragDevices = dragDevices,
_pointerAxisModifiers = pointerAxisModifiers;
final ScrollBehavior delegate; final ScrollBehavior delegate;
final bool scrollbars; final bool scrollbars;
...@@ -274,12 +298,16 @@ class _WrappedScrollBehavior implements ScrollBehavior { ...@@ -274,12 +298,16 @@ class _WrappedScrollBehavior implements ScrollBehavior {
final ScrollPhysics? physics; final ScrollPhysics? physics;
final TargetPlatform? platform; final TargetPlatform? platform;
final Set<PointerDeviceKind>? _dragDevices; final Set<PointerDeviceKind>? _dragDevices;
final Set<LogicalKeyboardKey>? _pointerAxisModifiers;
@override @override
final AndroidOverscrollIndicator? _androidOverscrollIndicator; final AndroidOverscrollIndicator? _androidOverscrollIndicator;
@override @override
Set<PointerDeviceKind> get dragDevices => _dragDevices ?? delegate.dragDevices; Set<PointerDeviceKind> get dragDevices => _dragDevices ?? delegate.dragDevices;
@override
Set<LogicalKeyboardKey> get pointerAxisModifiers => _pointerAxisModifiers ?? delegate.pointerAxisModifiers;
@override @override
AndroidOverscrollIndicator get androidOverscrollIndicator => _androidOverscrollIndicator ?? delegate.androidOverscrollIndicator; AndroidOverscrollIndicator get androidOverscrollIndicator => _androidOverscrollIndicator ?? delegate.androidOverscrollIndicator;
...@@ -303,17 +331,19 @@ class _WrappedScrollBehavior implements ScrollBehavior { ...@@ -303,17 +331,19 @@ class _WrappedScrollBehavior implements ScrollBehavior {
ScrollBehavior copyWith({ ScrollBehavior copyWith({
bool? scrollbars, bool? scrollbars,
bool? overscroll, bool? overscroll,
Set<PointerDeviceKind>? dragDevices,
Set<LogicalKeyboardKey>? pointerAxisModifiers,
ScrollPhysics? physics, ScrollPhysics? physics,
TargetPlatform? platform, TargetPlatform? platform,
Set<PointerDeviceKind>? dragDevices,
AndroidOverscrollIndicator? androidOverscrollIndicator AndroidOverscrollIndicator? androidOverscrollIndicator
}) { }) {
return delegate.copyWith( return delegate.copyWith(
scrollbars: scrollbars ?? this.scrollbars, scrollbars: scrollbars ?? this.scrollbars,
overscroll: overscroll ?? this.overscroll, overscroll: overscroll ?? this.overscroll,
dragDevices: dragDevices ?? this.dragDevices,
pointerAxisModifiers: pointerAxisModifiers ?? this.pointerAxisModifiers,
physics: physics ?? this.physics, physics: physics ?? this.physics,
platform: platform ?? this.platform, platform: platform ?? this.platform,
dragDevices: dragDevices ?? this.dragDevices,
androidOverscrollIndicator: androidOverscrollIndicator ?? this.androidOverscrollIndicator, androidOverscrollIndicator: androidOverscrollIndicator ?? this.androidOverscrollIndicator,
); );
} }
...@@ -333,9 +363,10 @@ class _WrappedScrollBehavior implements ScrollBehavior { ...@@ -333,9 +363,10 @@ class _WrappedScrollBehavior implements ScrollBehavior {
return oldDelegate.delegate.runtimeType != delegate.runtimeType return oldDelegate.delegate.runtimeType != delegate.runtimeType
|| oldDelegate.scrollbars != scrollbars || oldDelegate.scrollbars != scrollbars
|| oldDelegate.overscroll != overscroll || oldDelegate.overscroll != overscroll
|| !setEquals<PointerDeviceKind>(oldDelegate.dragDevices, dragDevices)
|| !setEquals<LogicalKeyboardKey>(oldDelegate.pointerAxisModifiers, pointerAxisModifiers)
|| 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);
} }
......
...@@ -756,12 +756,32 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R ...@@ -756,12 +756,32 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
); );
} }
// Returns the delta that should result from applying [event] with axis and // Returns the delta that should result from applying [event] with axis,
// direction taken into account. // direction, and any modifiers specified by the ScrollBehavior taken into
// account.
double _pointerSignalEventDelta(PointerScrollEvent event) { double _pointerSignalEventDelta(PointerScrollEvent event) {
double delta = widget.axis == Axis.horizontal late double delta;
final Set<LogicalKeyboardKey> pressed = HardwareKeyboard.instance.logicalKeysPressed;
final bool flipAxes = pressed.any(_configuration.pointerAxisModifiers.contains) &&
// Axes are only flipped for physical mouse wheel input.
// On some platforms, like web, trackpad input is handled through pointer
// signals, but should not be included in this axis modifying behavior.
// This is because on a trackpad, all directional axes are available to
// the user, while mouse scroll wheels typically are restricted to one
// axis.
event.kind == PointerDeviceKind.mouse;
switch (widget.axis) {
case Axis.horizontal:
delta = flipAxes
? event.scrollDelta.dy
: event.scrollDelta.dx;
break;
case Axis.vertical:
delta = flipAxes
? event.scrollDelta.dx ? event.scrollDelta.dx
: event.scrollDelta.dy; : event.scrollDelta.dy;
}
if (axisDirectionIsReversed(widget.axisDirection)) { if (axisDirectionIsReversed(widget.axisDirection)) {
delta *= -1; delta *= -1;
......
...@@ -16,13 +16,17 @@ Future<void> pumpTest( ...@@ -16,13 +16,17 @@ Future<void> pumpTest(
TargetPlatform? platform, { TargetPlatform? platform, {
bool scrollable = true, bool scrollable = true,
bool reverse = false, bool reverse = false,
Set<LogicalKeyboardKey>? axisModifier,
Axis scrollDirection = Axis.vertical,
ScrollController? controller, ScrollController? controller,
bool enableMouseDrag = true, bool enableMouseDrag = true,
}) async { }) async {
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
scrollBehavior: const NoScrollbarBehavior().copyWith(dragDevices: enableMouseDrag scrollBehavior: const NoScrollbarBehavior().copyWith(
dragDevices: enableMouseDrag
? <ui.PointerDeviceKind>{...ui.PointerDeviceKind.values} ? <ui.PointerDeviceKind>{...ui.PointerDeviceKind.values}
: null, : null,
pointerAxisModifiers: axisModifier,
), ),
theme: ThemeData( theme: ThemeData(
platform: platform, platform: platform,
...@@ -30,9 +34,13 @@ Future<void> pumpTest( ...@@ -30,9 +34,13 @@ Future<void> pumpTest(
home: CustomScrollView( home: CustomScrollView(
controller: controller, controller: controller,
reverse: reverse, reverse: reverse,
scrollDirection: scrollDirection,
physics: scrollable ? null : const NeverScrollableScrollPhysics(), physics: scrollable ? null : const NeverScrollableScrollPhysics(),
slivers: const <Widget>[ slivers: <Widget>[
SliverToBoxAdapter(child: SizedBox(height: 2000.0)), SliverToBoxAdapter(child: SizedBox(
height: scrollDirection == Axis.vertical ? 2000.0 : null,
width: scrollDirection == Axis.horizontal ? 2000.0 : null,
)),
], ],
), ),
)); ));
...@@ -399,6 +407,118 @@ void main() { ...@@ -399,6 +407,118 @@ void main() {
expect(getScrollOffset(tester), 20.0); expect(getScrollOffset(tester), 20.0);
}); });
testWidgets('Scrolls horizontally when shift is pressed by default', (WidgetTester tester) async {
await pumpTest(
tester,
debugDefaultTargetPlatformOverride,
scrollDirection: Axis.horizontal,
);
final Offset scrollEventLocation = tester.getCenter(find.byType(Viewport));
final TestPointer testPointer = TestPointer(1, ui.PointerDeviceKind.mouse);
// Create a hover event so that |testPointer| has a location when generating the scroll.
testPointer.hover(scrollEventLocation);
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
// Vertical input not accepted
expect(getScrollOffset(tester), 0.0);
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
// Vertical input flipped to horizontal and accepted.
expect(getScrollOffset(tester), 20.0);
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
await tester.pump();
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
// Vertical input not accepted
expect(getScrollOffset(tester), 20.0);
}, variant: TargetPlatformVariant.all());
testWidgets('Scroll axis is not flipped for trackpad', (WidgetTester tester) async {
await pumpTest(
tester,
debugDefaultTargetPlatformOverride,
scrollDirection: Axis.horizontal,
);
final Offset scrollEventLocation = tester.getCenter(find.byType(Viewport));
final TestPointer testPointer = TestPointer(1, ui.PointerDeviceKind.trackpad);
// Create a hover event so that |testPointer| has a location when generating the scroll.
testPointer.hover(scrollEventLocation);
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
// Vertical input not accepted
expect(getScrollOffset(tester), 0.0);
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
// Vertical input not flipped.
expect(getScrollOffset(tester), 0.0);
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
await tester.pump();
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
// Vertical input not accepted
expect(getScrollOffset(tester), 0.0);
}, variant: TargetPlatformVariant.all());
testWidgets('Scrolls horizontally when custom key is pressed', (WidgetTester tester) async {
await pumpTest(
tester,
debugDefaultTargetPlatformOverride,
scrollDirection: Axis.horizontal,
axisModifier: <LogicalKeyboardKey>{ LogicalKeyboardKey.altLeft },
);
final Offset scrollEventLocation = tester.getCenter(find.byType(Viewport));
final TestPointer testPointer = TestPointer(1, ui.PointerDeviceKind.mouse);
// Create a hover event so that |testPointer| has a location when generating the scroll.
testPointer.hover(scrollEventLocation);
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
// Vertical input not accepted
expect(getScrollOffset(tester), 0.0);
await tester.sendKeyDownEvent(LogicalKeyboardKey.altLeft);
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
// Vertical input flipped to horizontal and accepted.
expect(getScrollOffset(tester), 20.0);
await tester.sendKeyUpEvent(LogicalKeyboardKey.altLeft);
await tester.pump();
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
// Vertical input not accepted
expect(getScrollOffset(tester), 20.0);
}, variant: TargetPlatformVariant.all());
testWidgets('Still scrolls horizontally when other keys are pressed at the same time', (WidgetTester tester) async {
await pumpTest(
tester,
debugDefaultTargetPlatformOverride,
scrollDirection: Axis.horizontal,
axisModifier: <LogicalKeyboardKey>{ LogicalKeyboardKey.altLeft },
);
final Offset scrollEventLocation = tester.getCenter(find.byType(Viewport));
final TestPointer testPointer = TestPointer(1, ui.PointerDeviceKind.mouse);
// Create a hover event so that |testPointer| has a location when generating the scroll.
testPointer.hover(scrollEventLocation);
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
// Vertical input not accepted
expect(getScrollOffset(tester), 0.0);
await tester.sendKeyDownEvent(LogicalKeyboardKey.altLeft);
await tester.sendKeyDownEvent(LogicalKeyboardKey.space);
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
// Vertical flipped & accepted.
expect(getScrollOffset(tester), 20.0);
await tester.sendKeyUpEvent(LogicalKeyboardKey.altLeft);
await tester.sendKeyUpEvent(LogicalKeyboardKey.space);
await tester.pump();
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
// Vertical input not accepted
expect(getScrollOffset(tester), 20.0);
}, variant: TargetPlatformVariant.all());
group('setCanDrag to false with active drag gesture: ', () { group('setCanDrag to false with active drag gesture: ', () {
Future<void> pumpTestWidget(WidgetTester tester, { required bool canDrag }) { Future<void> pumpTestWidget(WidgetTester tester, { required bool canDrag }) {
return tester.pumpWidget( return tester.pumpWidget(
......
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