Unverified Commit 5922a40e authored by stuartmorgan's avatar stuartmorgan Committed by GitHub

Add support for scrollwheels (#22762)

Adds support for discrete scroll events, such as those sent by a scroll wheel.

Includes the plumbing to convert, dispatch, and handle these events, as well as
Scrollable support for consuming them.
parent c78ccb0b
...@@ -25,6 +25,7 @@ export 'src/gestures/mouse_tracking.dart'; ...@@ -25,6 +25,7 @@ export 'src/gestures/mouse_tracking.dart';
export 'src/gestures/multidrag.dart'; export 'src/gestures/multidrag.dart';
export 'src/gestures/multitap.dart'; export 'src/gestures/multitap.dart';
export 'src/gestures/pointer_router.dart'; export 'src/gestures/pointer_router.dart';
export 'src/gestures/pointer_signal_resolver.dart';
export 'src/gestures/recognizer.dart'; export 'src/gestures/recognizer.dart';
export 'src/gestures/scale.dart'; export 'src/gestures/scale.dart';
export 'src/gestures/tap.dart'; export 'src/gestures/tap.dart';
......
...@@ -14,6 +14,7 @@ import 'debug.dart'; ...@@ -14,6 +14,7 @@ import 'debug.dart';
import 'events.dart'; import 'events.dart';
import 'hit_test.dart'; import 'hit_test.dart';
import 'pointer_router.dart'; import 'pointer_router.dart';
import 'pointer_signal_resolver.dart';
/// A binding for the gesture subsystem. /// A binding for the gesture subsystem.
/// ///
...@@ -108,6 +109,10 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H ...@@ -108,6 +109,10 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
/// pointer events. /// pointer events.
final GestureArenaManager gestureArena = GestureArenaManager(); final GestureArenaManager gestureArena = GestureArenaManager();
/// The resolver used for determining which widget handles a pointer
/// signal event.
final PointerSignalResolver pointerSignalResolver = PointerSignalResolver();
/// State for all pointers which are currently down. /// State for all pointers which are currently down.
/// ///
/// The state of hovering pointers is not tracked because that would require /// The state of hovering pointers is not tracked because that would require
...@@ -117,11 +122,13 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H ...@@ -117,11 +122,13 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
void _handlePointerEvent(PointerEvent event) { void _handlePointerEvent(PointerEvent event) {
assert(!locked); assert(!locked);
HitTestResult hitTestResult; HitTestResult hitTestResult;
if (event is PointerDownEvent) { if (event is PointerDownEvent || event is PointerSignalEvent) {
assert(!_hitTests.containsKey(event.pointer)); assert(!_hitTests.containsKey(event.pointer));
hitTestResult = HitTestResult(); hitTestResult = HitTestResult();
hitTest(hitTestResult, event.position); hitTest(hitTestResult, event.position);
if (event is PointerDownEvent) {
_hitTests[event.pointer] = hitTestResult; _hitTests[event.pointer] = hitTestResult;
}
assert(() { assert(() {
if (debugPrintHitTestResults) if (debugPrintHitTestResults)
debugPrint('$event: $hitTestResult'); debugPrint('$event: $hitTestResult');
...@@ -216,6 +223,8 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H ...@@ -216,6 +223,8 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
gestureArena.close(event.pointer); gestureArena.close(event.pointer);
} else if (event is PointerUpEvent) { } else if (event is PointerUpEvent) {
gestureArena.sweep(event.pointer); gestureArena.sweep(event.pointer);
} else if (event is PointerSignalEvent) {
pointerSignalResolver.resolve(event);
} }
} }
} }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:ui' as ui show PointerData, PointerChange; import 'dart:ui' as ui show PointerData, PointerChange, PointerSignalKind;
import 'package:flutter/foundation.dart' show visibleForTesting; import 'package:flutter/foundation.dart' show visibleForTesting;
...@@ -82,6 +82,7 @@ class PointerEventConverter { ...@@ -82,6 +82,7 @@ class PointerEventConverter {
final Duration timeStamp = datum.timeStamp; final Duration timeStamp = datum.timeStamp;
final PointerDeviceKind kind = datum.kind; final PointerDeviceKind kind = datum.kind;
assert(datum.change != null); assert(datum.change != null);
if (datum.signalKind == null || datum.signalKind == ui.PointerSignalKind.none) {
switch (datum.change) { switch (datum.change) {
case ui.PointerChange.add: case ui.PointerChange.add:
assert(!_pointers.containsKey(datum.device)); assert(!_pointers.containsKey(datum.device));
...@@ -378,6 +379,83 @@ class PointerEventConverter { ...@@ -378,6 +379,83 @@ class PointerEventConverter {
); );
break; break;
} }
} else {
switch (datum.signalKind) {
case ui.PointerSignalKind.scroll:
// Devices must be added before they send scroll events.
assert(_pointers.containsKey(datum.device));
final _PointerState state = _ensureStateForPointer(datum, position);
if (state.lastPosition != position) {
// Synthesize a hover/move of the pointer to the scroll location
// before sending the scroll event, if necessary, so that clients
// don't have to worry about native ordering of hover and scroll
// events.
final Offset offset = position - state.lastPosition;
state.lastPosition = position;
if (state.down) {
yield PointerMoveEvent(
timeStamp: timeStamp,
pointer: state.pointer,
kind: kind,
device: datum.device,
position: position,
delta: offset,
buttons: datum.buttons,
obscured: datum.obscured,
pressureMin: datum.pressureMin,
pressureMax: datum.pressureMax,
distanceMax: datum.distanceMax,
size: datum.size,
radiusMajor: radiusMajor,
radiusMinor: radiusMinor,
radiusMin: radiusMin,
radiusMax: radiusMax,
orientation: datum.orientation,
tilt: datum.tilt,
synthesized: true,
);
} else {
yield PointerHoverEvent(
timeStamp: timeStamp,
kind: kind,
device: datum.device,
position: position,
delta: offset,
buttons: datum.buttons,
obscured: datum.obscured,
pressureMin: datum.pressureMin,
pressureMax: datum.pressureMax,
distance: datum.distance,
distanceMax: datum.distanceMax,
size: datum.size,
radiusMajor: radiusMajor,
radiusMinor: radiusMinor,
radiusMin: radiusMin,
radiusMax: radiusMax,
orientation: datum.orientation,
tilt: datum.tilt,
synthesized: true,
);
}
}
final Offset scrollDelta =
Offset(datum.scrollDeltaX, datum.scrollDeltaY) / devicePixelRatio;
yield PointerScrollEvent(
timeStamp: timeStamp,
kind: kind,
device: datum.device,
position: position,
scrollDelta: scrollDelta,
);
break;
case ui.PointerSignalKind.none:
assert(false); // This branch should already have 'none' filtered out.
break;
case ui.PointerSignalKind.unknown:
// Ignore unknown signals.
break;
}
}
} }
} }
......
...@@ -790,6 +790,65 @@ class PointerUpEvent extends PointerEvent { ...@@ -790,6 +790,65 @@ class PointerUpEvent extends PointerEvent {
); );
} }
/// An event that corresponds to a discrete pointer signal.
///
/// Pointer signals are events that originate from the pointer but don't change
/// the state of the pointer itself, and are discrete rather than needing to be
/// interpreted in the context of a series of events.
abstract class PointerSignalEvent extends PointerEvent {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const PointerSignalEvent({
Duration timeStamp = Duration.zero,
int pointer = 0,
PointerDeviceKind kind = PointerDeviceKind.mouse,
int device = 0,
Offset position = Offset.zero,
}) : super(
timeStamp: timeStamp,
pointer: pointer,
kind: kind,
device: device,
position: position,
);
}
/// The pointer issued a scroll event.
///
/// Scrolling the scroll wheel on a mouse is an example of an event that
/// would create a [PointerScrollEvent].
class PointerScrollEvent extends PointerSignalEvent {
/// Creates a pointer scroll event.
///
/// All of the arguments must be non-null.
const PointerScrollEvent({
Duration timeStamp = Duration.zero,
PointerDeviceKind kind = PointerDeviceKind.mouse,
int device = 0,
Offset position = Offset.zero,
this.scrollDelta = Offset.zero,
}) : assert(timeStamp != null),
assert(kind != null),
assert(device != null),
assert(position != null),
assert(scrollDelta != null),
super(
timeStamp: timeStamp,
kind: kind,
device: device,
position: position,
);
/// The amount to scroll, in logical pixels.
final Offset scrollDelta;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<Offset>('scrollDelta', scrollDelta));
}
}
/// The input from the pointer is no longer directed towards this receiver. /// The input from the pointer is no longer directed towards this receiver.
class PointerCancelEvent extends PointerEvent { class PointerCancelEvent extends PointerEvent {
/// Creates a pointer cancel event. /// Creates a pointer cancel event.
......
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'events.dart';
/// The callback to register with a [PointerSignalResolver] to express
/// interest in a pointer signal event.
typedef PointerSignalResolvedCallback = void Function(PointerSignalEvent event);
/// An resolver for pointer signal events.
///
/// Objects interested in a [PointerSignalEvent] should register a callback to
/// be called if they should handle the event. The resolver's purpose is to
/// ensure that the same pointer signal is not handled by multiple objects in
/// a hierarchy.
///
/// Pointer signals are immediate, so unlike a gesture arena it always resolves
/// at the end of event dispatch. The first callback registered will be the one
/// that is called.
class PointerSignalResolver {
PointerSignalResolvedCallback _firstRegisteredCallback;
PointerSignalEvent _currentEvent;
/// Registers interest in handling [event].
void register(PointerSignalEvent event, PointerSignalResolvedCallback callback) {
assert(event != null);
assert(callback != null);
assert(_currentEvent == null || _currentEvent == event);
if (_firstRegisteredCallback != null) {
return;
}
_currentEvent = event;
_firstRegisteredCallback = callback;
}
/// Resolves the event, calling the first registered callback if there was
/// one.
///
/// Called after the framework has finished dispatching the pointer signal
/// event.
void resolve(PointerSignalEvent event) {
if (_firstRegisteredCallback == null) {
assert(_currentEvent == null);
return;
}
assert(_currentEvent == event);
try {
_firstRegisteredCallback(event);
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'gesture library',
context: 'while resolving a PointerSignalEvent',
informationCollector: (StringBuffer information) {
information.writeln('Event:');
information.write(' $event');
}
));
}
_firstRegisteredCallback = null;
_currentEvent = null;
}
}
...@@ -2488,6 +2488,11 @@ typedef PointerUpEventListener = void Function(PointerUpEvent event); ...@@ -2488,6 +2488,11 @@ typedef PointerUpEventListener = void Function(PointerUpEvent event);
/// Used by [Listener] and [RenderPointerListener]. /// Used by [Listener] and [RenderPointerListener].
typedef PointerCancelEventListener = void Function(PointerCancelEvent event); typedef PointerCancelEventListener = void Function(PointerCancelEvent event);
/// Signature for listening to [PointerSignalEvent] events.
///
/// Used by [Listener] and [RenderPointerListener].
typedef PointerSignalEventListener = void Function(PointerSignalEvent event);
/// Calls callbacks in response to pointer events. /// Calls callbacks in response to pointer events.
/// ///
/// If it has a child, defers to the child for sizing behavior. /// If it has a child, defers to the child for sizing behavior.
...@@ -2509,6 +2514,7 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior { ...@@ -2509,6 +2514,7 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior {
PointerExitEventListener onPointerExit, PointerExitEventListener onPointerExit,
this.onPointerUp, this.onPointerUp,
this.onPointerCancel, this.onPointerCancel,
this.onPointerSignal,
HitTestBehavior behavior = HitTestBehavior.deferToChild, HitTestBehavior behavior = HitTestBehavior.deferToChild,
RenderBox child, RenderBox child,
}) : _onPointerEnter = onPointerEnter, }) : _onPointerEnter = onPointerEnter,
...@@ -2579,6 +2585,9 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior { ...@@ -2579,6 +2585,9 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior {
/// no longer directed towards this receiver. /// no longer directed towards this receiver.
PointerCancelEventListener onPointerCancel; PointerCancelEventListener onPointerCancel;
/// Called when a pointer signal occures over this object.
PointerSignalEventListener onPointerSignal;
// Object used for annotation of the layer used for hover hit detection. // Object used for annotation of the layer used for hover hit detection.
MouseTrackerAnnotation _hoverAnnotation; MouseTrackerAnnotation _hoverAnnotation;
...@@ -2647,6 +2656,8 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior { ...@@ -2647,6 +2656,8 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior {
return onPointerUp(event); return onPointerUp(event);
if (onPointerCancel != null && event is PointerCancelEvent) if (onPointerCancel != null && event is PointerCancelEvent)
return onPointerCancel(event); return onPointerCancel(event);
if (onPointerSignal != null && event is PointerSignalEvent)
return onPointerSignal(event);
} }
@override @override
...@@ -2667,6 +2678,8 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior { ...@@ -2667,6 +2678,8 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior {
listeners.add('up'); listeners.add('up');
if (onPointerCancel != null) if (onPointerCancel != null)
listeners.add('cancel'); listeners.add('cancel');
if (onPointerSignal != null)
listeners.add('signal');
if (listeners.isEmpty) if (listeners.isEmpty)
listeners.add('<none>'); listeners.add('<none>');
properties.add(IterableProperty<String>('listeners', listeners)); properties.add(IterableProperty<String>('listeners', listeners));
......
...@@ -5240,6 +5240,7 @@ class Listener extends SingleChildRenderObjectWidget { ...@@ -5240,6 +5240,7 @@ class Listener extends SingleChildRenderObjectWidget {
this.onPointerHover, this.onPointerHover,
this.onPointerUp, this.onPointerUp,
this.onPointerCancel, this.onPointerCancel,
this.onPointerSignal,
this.behavior = HitTestBehavior.deferToChild, this.behavior = HitTestBehavior.deferToChild,
Widget child, Widget child,
}) : assert(behavior != null), }) : assert(behavior != null),
...@@ -5288,6 +5289,9 @@ class Listener extends SingleChildRenderObjectWidget { ...@@ -5288,6 +5289,9 @@ class Listener extends SingleChildRenderObjectWidget {
/// no longer directed towards this receiver. /// no longer directed towards this receiver.
final PointerCancelEventListener onPointerCancel; final PointerCancelEventListener onPointerCancel;
/// Called when a pointer signal occurs over this object.
final PointerSignalEventListener onPointerSignal;
/// How to behave during hit testing. /// How to behave during hit testing.
final HitTestBehavior behavior; final HitTestBehavior behavior;
...@@ -5301,6 +5305,7 @@ class Listener extends SingleChildRenderObjectWidget { ...@@ -5301,6 +5305,7 @@ class Listener extends SingleChildRenderObjectWidget {
onPointerExit: onPointerExit, onPointerExit: onPointerExit,
onPointerUp: onPointerUp, onPointerUp: onPointerUp,
onPointerCancel: onPointerCancel, onPointerCancel: onPointerCancel,
onPointerSignal: onPointerSignal,
behavior: behavior, behavior: behavior,
); );
} }
...@@ -5315,6 +5320,7 @@ class Listener extends SingleChildRenderObjectWidget { ...@@ -5315,6 +5320,7 @@ class Listener extends SingleChildRenderObjectWidget {
..onPointerExit = onPointerExit ..onPointerExit = onPointerExit
..onPointerUp = onPointerUp ..onPointerUp = onPointerUp
..onPointerCancel = onPointerCancel ..onPointerCancel = onPointerCancel
..onPointerSignal = onPointerSignal
..behavior = behavior; ..behavior = behavior;
} }
...@@ -5336,6 +5342,8 @@ class Listener extends SingleChildRenderObjectWidget { ...@@ -5336,6 +5342,8 @@ class Listener extends SingleChildRenderObjectWidget {
listeners.add('up'); listeners.add('up');
if (onPointerCancel != null) if (onPointerCancel != null)
listeners.add('cancel'); listeners.add('cancel');
if (onPointerSignal != null)
listeners.add('signal');
properties.add(IterableProperty<String>('listeners', listeners, ifEmpty: '<none>')); properties.add(IterableProperty<String>('listeners', listeners, ifEmpty: '<none>'));
properties.add(EnumProperty<HitTestBehavior>('behavior', behavior)); properties.add(EnumProperty<HitTestBehavior>('behavior', behavior));
} }
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:math' as math;
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
...@@ -520,6 +521,35 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin ...@@ -520,6 +521,35 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
_drag = null; _drag = null;
} }
// SCROLL WHEEL
// Returns the offset that should result from applying [event] to the current
// position, taking min/max scroll extent into account.
double _targetScrollOffsetForPointerScroll(PointerScrollEvent event) {
final double delta = widget.axis == Axis.horizontal
? event.scrollDelta.dx
: event.scrollDelta.dy;
return math.min(math.max(position.pixels + delta, position.minScrollExtent),
position.maxScrollExtent);
}
void _receivedPointerSignal(PointerSignalEvent event) {
if (event is PointerScrollEvent && position != null) {
final double targetScrollOffset = _targetScrollOffsetForPointerScroll(event);
// Only express interest in the event if it would actually result in a scroll.
if (targetScrollOffset != position.pixels) {
GestureBinding.instance.pointerSignalResolver.register(event, _handlePointerScroll);
}
}
}
void _handlePointerScroll(PointerEvent event) {
assert(event is PointerScrollEvent);
final double targetScrollOffset = _targetScrollOffsetForPointerScroll(event);
if (targetScrollOffset != position.pixels) {
position.jumpTo(targetScrollOffset);
}
}
// DESCRIPTION // DESCRIPTION
...@@ -538,6 +568,8 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin ...@@ -538,6 +568,8 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
scrollable: this, scrollable: this,
position: position, position: position,
// TODO(ianh): Having all these global keys is sad. // TODO(ianh): Having all these global keys is sad.
child: Listener(
onPointerSignal: _receivedPointerSignal,
child: RawGestureDetector( child: RawGestureDetector(
key: _gestureDetectorKey, key: _gestureDetectorKey,
gestures: _gestureRecognizers, gestures: _gestureRecognizers,
...@@ -553,6 +585,7 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin ...@@ -553,6 +585,7 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
), ),
), ),
), ),
),
); );
if (!widget.excludeFromSemantics) { if (!widget.excludeFromSemantics) {
......
...@@ -203,4 +203,50 @@ void main() { ...@@ -203,4 +203,50 @@ void main() {
expect(events[3].runtimeType, equals(PointerCancelEvent)); expect(events[3].runtimeType, equals(PointerCancelEvent));
expect(events[4].runtimeType, equals(PointerRemovedEvent)); expect(events[4].runtimeType, equals(PointerRemovedEvent));
}); });
test('Can expand pointer scroll events', () {
const ui.PointerDataPacket packet = ui.PointerDataPacket(
data: <ui.PointerData>[
ui.PointerData(change: ui.PointerChange.add),
ui.PointerData(change: ui.PointerChange.hover, signalKind: ui.PointerSignalKind.scroll),
]
);
final List<PointerEvent> events = PointerEventConverter.expand(
packet.data, ui.window.devicePixelRatio).toList();
expect(events.length, 2);
expect(events[0].runtimeType, equals(PointerAddedEvent));
expect(events[1].runtimeType, equals(PointerScrollEvent));
});
test('Synthetic hover/move for misplaced scrolls', () {
final Offset lastLocation = const Offset(10.0, 10.0) * ui.window.devicePixelRatio;
const Offset unexpectedOffset = Offset(5.0, 7.0);
final Offset scrollLocation = lastLocation + unexpectedOffset * ui.window.devicePixelRatio;
final ui.PointerDataPacket packet = ui.PointerDataPacket(
data: <ui.PointerData>[
ui.PointerData(change: ui.PointerChange.add, physicalX: lastLocation.dx, physicalY: lastLocation.dy),
ui.PointerData(change: ui.PointerChange.hover, physicalX: scrollLocation.dx, physicalY: scrollLocation.dy, signalKind: ui.PointerSignalKind.scroll),
// Move back to starting location, click, and repeat to test mouse-down version.
ui.PointerData(change: ui.PointerChange.hover, physicalX: lastLocation.dx, physicalY: lastLocation.dy),
ui.PointerData(change: ui.PointerChange.down, physicalX: lastLocation.dx, physicalY: lastLocation.dy),
ui.PointerData(change: ui.PointerChange.hover, physicalX: scrollLocation.dx, physicalY: scrollLocation.dy, signalKind: ui.PointerSignalKind.scroll),
]
);
final List<PointerEvent> events = PointerEventConverter.expand(
packet.data, ui.window.devicePixelRatio).toList();
expect(events.length, 7);
expect(events[0].runtimeType, equals(PointerAddedEvent));
expect(events[1].runtimeType, equals(PointerHoverEvent));
expect(events[1].delta, equals(unexpectedOffset));
expect(events[2].runtimeType, equals(PointerScrollEvent));
expect(events[3].runtimeType, equals(PointerHoverEvent));
expect(events[4].runtimeType, equals(PointerDownEvent));
expect(events[5].runtimeType, equals(PointerMoveEvent));
expect(events[5].delta, equals(unexpectedOffset));
expect(events[6].runtimeType, equals(PointerScrollEvent));
});
} }
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/gestures.dart';
import '../flutter_test_alternative.dart';
class TestPointerSignalListener {
TestPointerSignalListener(this.event);
final PointerSignalEvent event;
bool callbackRan = false;
void callback(PointerSignalEvent event) {
expect(event, equals(this.event));
expect(callbackRan, isFalse);
callbackRan = true;
}
}
class PointerSignalTester {
final PointerSignalResolver resolver = PointerSignalResolver();
PointerSignalEvent event = const PointerScrollEvent();
TestPointerSignalListener addListener() {
final TestPointerSignalListener listener = TestPointerSignalListener(event);
resolver.register(event, listener.callback);
return listener;
}
/// Simulates a new event dispatch cycle by resolving the current event and
/// setting a new event to use for future calls.
void resolve() {
resolver.resolve(event);
event = const PointerScrollEvent();
}
}
void main() {
test('Resolving with no entries should be a no-op', () {
final PointerSignalTester tester = PointerSignalTester();
tester.resolver.resolve(tester.event);
});
test('First entry should always win', () {
final PointerSignalTester tester = PointerSignalTester();
final TestPointerSignalListener first = tester.addListener();
final TestPointerSignalListener second = tester.addListener();
tester.resolve();
expect(first.callbackRan, isTrue);
expect(second.callbackRan, isFalse);
});
test('Re-use after resolve should work', () {
final PointerSignalTester tester = PointerSignalTester();
final TestPointerSignalListener first = tester.addListener();
final TestPointerSignalListener second = tester.addListener();
tester.resolve();
expect(first.callbackRan, isTrue);
expect(second.callbackRan, isFalse);
final TestPointerSignalListener newEventListener = tester.addListener();
tester.resolve();
expect(newEventListener.callbackRan, isTrue);
// Nothing should have changed for the previous event's listeners.
expect(first.callbackRan, isTrue);
expect(second.callbackRan, isFalse);
});
}
...@@ -236,6 +236,13 @@ void main() { ...@@ -236,6 +236,13 @@ void main() {
' │ semantic boundary\n' ' │ semantic boundary\n'
' │ size: Size(800.0, 600.0)\n' ' │ size: Size(800.0, 600.0)\n'
' │\n' ' │\n'
' └─child: RenderPointerListener#00000\n'
' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' │ size: Size(800.0, 600.0)\n'
' │ behavior: deferToChild\n'
' │ listeners: signal\n'
' │\n'
' └─child: RenderSemanticsGestureHandler#00000\n' ' └─child: RenderSemanticsGestureHandler#00000\n'
' │ parentData: <none> (can use size)\n' ' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n' ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
...@@ -365,6 +372,13 @@ void main() { ...@@ -365,6 +372,13 @@ void main() {
' │ semantic boundary\n' ' │ semantic boundary\n'
' │ size: Size(800.0, 600.0)\n' ' │ size: Size(800.0, 600.0)\n'
' │\n' ' │\n'
' └─child: RenderPointerListener#00000\n'
' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' │ size: Size(800.0, 600.0)\n'
' │ behavior: deferToChild\n'
' │ listeners: signal\n'
' │\n'
' └─child: RenderSemanticsGestureHandler#00000\n' ' └─child: RenderSemanticsGestureHandler#00000\n'
' │ parentData: <none> (can use size)\n' ' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n' ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:ui' as ui;
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
...@@ -220,4 +222,18 @@ void main() { ...@@ -220,4 +222,18 @@ void main() {
await gesture.moveBy(const Offset(0.0, -1.0), timeStamp: const Duration(milliseconds: 180)); await gesture.moveBy(const Offset(0.0, -1.0), timeStamp: const Duration(milliseconds: 180));
expect(getScrollOffset(tester), 32.5); expect(getScrollOffset(tester), 32.5);
}); });
testWidgets('Scroll pointer signals are handled', (WidgetTester tester) async {
await pumpTest(tester, TargetPlatform.fuchsia);
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);
final HitTestResult result = tester.hitTestOnBinding(scrollEventLocation);
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)), result);
expect(getScrollOffset(tester), 20.0);
// Pointer signals should not cause overscroll.
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, -30.0)), result);
expect(getScrollOffset(tester), 0.0);
});
} }
...@@ -169,6 +169,26 @@ class TestPointer { ...@@ -169,6 +169,26 @@ class TestPointer {
delta: delta, delta: delta,
); );
} }
/// Create a [PointerScrollEvent] (e.g., scroll wheel scroll; not finger-drag
/// scroll) with the given delta.
///
/// By default, the time stamp on the event is [Duration.zero]. You can give a
/// specific time stamp by passing the `timeStamp` argument.
PointerScrollEvent scroll(
Offset scrollDelta, {
Duration timeStamp = Duration.zero,
}) {
assert(scrollDelta != null);
assert(timeStamp != null);
assert(kind != PointerDeviceKind.touch, "Touch pointers can't generate pointer signal events");
return PointerScrollEvent(
timeStamp: timeStamp,
kind: kind,
position: location,
scrollDelta: scrollDelta,
);
}
} }
/// Signature for a callback that can dispatch events and returns a future that /// Signature for a callback that can dispatch events and returns a future that
......
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