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 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart' show LogicalKeyboardKey;
import 'framework.dart';
import 'overscroll_indicator.dart';
......@@ -100,6 +101,7 @@ class ScrollBehavior {
bool? scrollbars,
bool? overscroll,
Set<PointerDeviceKind>? dragDevices,
Set<LogicalKeyboardKey>? pointerAxisModifiers,
ScrollPhysics? physics,
TargetPlatform? platform,
@Deprecated(
......@@ -112,9 +114,10 @@ class ScrollBehavior {
delegate: this,
scrollbars: scrollbars ?? true,
overscroll: overscroll ?? true,
dragDevices: dragDevices,
pointerAxisModifiers: pointerAxisModifiers,
physics: physics,
platform: platform,
dragDevices: dragDevices,
androidOverscrollIndicator: androidOverscrollIndicator
);
}
......@@ -132,6 +135,25 @@ class ScrollBehavior {
/// impossible to select text in scrollable containers and is not recommended.
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.
Widget buildScrollbar(BuildContext context, Widget child, ScrollableDetails details) {
// When modifying this function, consider modifying the implementation in
......@@ -261,12 +283,14 @@ class _WrappedScrollBehavior implements ScrollBehavior {
required this.delegate,
this.scrollbars = true,
this.overscroll = true,
Set<PointerDeviceKind>? dragDevices,
Set<LogicalKeyboardKey>? pointerAxisModifiers,
this.physics,
this.platform,
Set<PointerDeviceKind>? dragDevices,
AndroidOverscrollIndicator? androidOverscrollIndicator,
}) : _androidOverscrollIndicator = androidOverscrollIndicator,
_dragDevices = dragDevices;
_dragDevices = dragDevices,
_pointerAxisModifiers = pointerAxisModifiers;
final ScrollBehavior delegate;
final bool scrollbars;
......@@ -274,12 +298,16 @@ class _WrappedScrollBehavior implements ScrollBehavior {
final ScrollPhysics? physics;
final TargetPlatform? platform;
final Set<PointerDeviceKind>? _dragDevices;
final Set<LogicalKeyboardKey>? _pointerAxisModifiers;
@override
final AndroidOverscrollIndicator? _androidOverscrollIndicator;
@override
Set<PointerDeviceKind> get dragDevices => _dragDevices ?? delegate.dragDevices;
@override
Set<LogicalKeyboardKey> get pointerAxisModifiers => _pointerAxisModifiers ?? delegate.pointerAxisModifiers;
@override
AndroidOverscrollIndicator get androidOverscrollIndicator => _androidOverscrollIndicator ?? delegate.androidOverscrollIndicator;
......@@ -303,17 +331,19 @@ class _WrappedScrollBehavior implements ScrollBehavior {
ScrollBehavior copyWith({
bool? scrollbars,
bool? overscroll,
Set<PointerDeviceKind>? dragDevices,
Set<LogicalKeyboardKey>? pointerAxisModifiers,
ScrollPhysics? physics,
TargetPlatform? platform,
Set<PointerDeviceKind>? dragDevices,
AndroidOverscrollIndicator? androidOverscrollIndicator
}) {
return delegate.copyWith(
scrollbars: scrollbars ?? this.scrollbars,
overscroll: overscroll ?? this.overscroll,
dragDevices: dragDevices ?? this.dragDevices,
pointerAxisModifiers: pointerAxisModifiers ?? this.pointerAxisModifiers,
physics: physics ?? this.physics,
platform: platform ?? this.platform,
dragDevices: dragDevices ?? this.dragDevices,
androidOverscrollIndicator: androidOverscrollIndicator ?? this.androidOverscrollIndicator,
);
}
......@@ -333,9 +363,10 @@ class _WrappedScrollBehavior implements ScrollBehavior {
return oldDelegate.delegate.runtimeType != delegate.runtimeType
|| oldDelegate.scrollbars != scrollbars
|| oldDelegate.overscroll != overscroll
|| !setEquals<PointerDeviceKind>(oldDelegate.dragDevices, dragDevices)
|| !setEquals<LogicalKeyboardKey>(oldDelegate.pointerAxisModifiers, pointerAxisModifiers)
|| oldDelegate.physics != physics
|| oldDelegate.platform != platform
|| !setEquals<PointerDeviceKind>(oldDelegate.dragDevices, dragDevices)
|| delegate.shouldNotify(oldDelegate.delegate);
}
......
......@@ -756,12 +756,32 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
);
}
// Returns the delta that should result from applying [event] with axis and
// direction taken into account.
// Returns the delta that should result from applying [event] with axis,
// direction, and any modifiers specified by the ScrollBehavior taken into
// account.
double _pointerSignalEventDelta(PointerScrollEvent event) {
double delta = widget.axis == Axis.horizontal
? event.scrollDelta.dx
: event.scrollDelta.dy;
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.dy;
}
if (axisDirectionIsReversed(widget.axisDirection)) {
delta *= -1;
......
......@@ -16,13 +16,17 @@ Future<void> pumpTest(
TargetPlatform? platform, {
bool scrollable = true,
bool reverse = false,
Set<LogicalKeyboardKey>? axisModifier,
Axis scrollDirection = Axis.vertical,
ScrollController? controller,
bool enableMouseDrag = true,
}) async {
await tester.pumpWidget(MaterialApp(
scrollBehavior: const NoScrollbarBehavior().copyWith(dragDevices: enableMouseDrag
? <ui.PointerDeviceKind>{...ui.PointerDeviceKind.values}
: null,
scrollBehavior: const NoScrollbarBehavior().copyWith(
dragDevices: enableMouseDrag
? <ui.PointerDeviceKind>{...ui.PointerDeviceKind.values}
: null,
pointerAxisModifiers: axisModifier,
),
theme: ThemeData(
platform: platform,
......@@ -30,9 +34,13 @@ Future<void> pumpTest(
home: CustomScrollView(
controller: controller,
reverse: reverse,
scrollDirection: scrollDirection,
physics: scrollable ? null : const NeverScrollableScrollPhysics(),
slivers: const <Widget>[
SliverToBoxAdapter(child: SizedBox(height: 2000.0)),
slivers: <Widget>[
SliverToBoxAdapter(child: SizedBox(
height: scrollDirection == Axis.vertical ? 2000.0 : null,
width: scrollDirection == Axis.horizontal ? 2000.0 : null,
)),
],
),
));
......@@ -399,6 +407,118 @@ void main() {
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: ', () {
Future<void> pumpTestWidget(WidgetTester tester, { required bool canDrag }) {
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