Unverified Commit 11e0a725 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Re-land: Add support for Tooltip hover (#31699)

This is a re-land of #31561, after fixing performance regressions.

Added change listening to the MouseTracker so that the Listener and tooltip can react to whether or not a mouse is connected at all. Added a change check to make sure Listener only repaints when something changed.

Fixes #22817
parent 3bd1737c
...@@ -9,6 +9,7 @@ import '../common.dart'; ...@@ -9,6 +9,7 @@ import '../common.dart';
const int _kNumIters = 10000; const int _kNumIters = 10000;
void main() { void main() {
assert(false, "Don't run benchmarks in checked mode! Use 'flutter run --release'.");
final Stopwatch watch = Stopwatch(); final Stopwatch watch = Stopwatch();
print('RRect contains benchmark...'); print('RRect contains benchmark...');
watch.start(); watch.start();
......
...@@ -10,6 +10,7 @@ import 'data/velocity_tracker_data.dart'; ...@@ -10,6 +10,7 @@ import 'data/velocity_tracker_data.dart';
const int _kNumIters = 10000; const int _kNumIters = 10000;
void main() { void main() {
assert(false, "Don't run benchmarks in checked mode! Use 'flutter run --release'.");
final VelocityTracker tracker = VelocityTracker(); final VelocityTracker tracker = VelocityTracker();
final Stopwatch watch = Stopwatch(); final Stopwatch watch = Stopwatch();
print('Velocity tracker benchmark...'); print('Velocity tracker benchmark...');
......
...@@ -34,7 +34,7 @@ class BenchmarkingBinding extends LiveTestWidgetsFlutterBinding { ...@@ -34,7 +34,7 @@ class BenchmarkingBinding extends LiveTestWidgetsFlutterBinding {
} }
Future<void> main() async { Future<void> main() async {
assert(false); // don't run this in checked mode! Use --release. assert(false, "Don't run benchmarks in checked mode! Use 'flutter run --release'.");
stock_data.StockData.actuallyFetchData = false; stock_data.StockData.actuallyFetchData = false;
final Stopwatch wallClockWatch = Stopwatch(); final Stopwatch wallClockWatch = Stopwatch();
......
...@@ -14,7 +14,7 @@ import '../common.dart'; ...@@ -14,7 +14,7 @@ import '../common.dart';
const Duration kBenchmarkTime = Duration(seconds: 15); const Duration kBenchmarkTime = Duration(seconds: 15);
Future<void> main() async { Future<void> main() async {
assert(false); // don't run this in checked mode! Use --release. assert(false, "Don't run benchmarks in checked mode! Use 'flutter run --release'.");
stock_data.StockData.actuallyFetchData = false; stock_data.StockData.actuallyFetchData = false;
// We control the framePolicy below to prevent us from scheduling frames in // We control the framePolicy below to prevent us from scheduling frames in
......
...@@ -15,6 +15,7 @@ import '../common.dart'; ...@@ -15,6 +15,7 @@ import '../common.dart';
const Duration kBenchmarkTime = Duration(seconds: 15); const Duration kBenchmarkTime = Duration(seconds: 15);
Future<void> main() async { Future<void> main() async {
assert(false, "Don't run benchmarks in checked mode! Use 'flutter run --release'.");
stock_data.StockData.actuallyFetchData = false; stock_data.StockData.actuallyFetchData = false;
// We control the framePolicy below to prevent us from scheduling frames in // We control the framePolicy below to prevent us from scheduling frames in
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/foundation.dart' show visibleForTesting; import 'package:flutter/foundation.dart' show ChangeNotifier, visibleForTesting;
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'events.dart'; import 'events.dart';
...@@ -84,8 +84,11 @@ typedef MouseDetectorAnnotationFinder = MouseTrackerAnnotation Function(Offset o ...@@ -84,8 +84,11 @@ typedef MouseDetectorAnnotationFinder = MouseTrackerAnnotation Function(Offset o
/// and notifies them when a mouse pointer enters, moves, or leaves an annotated /// and notifies them when a mouse pointer enters, moves, or leaves an annotated
/// region that they are interested in. /// region that they are interested in.
/// ///
/// This class is a [ChangeNotifier] that notifies its listeners if the value of
/// [mouseIsConnected] changes.
///
/// Owned by the [RendererBinding] class. /// Owned by the [RendererBinding] class.
class MouseTracker { class MouseTracker extends ChangeNotifier {
/// Creates a mouse tracker to keep track of mouse locations. /// Creates a mouse tracker to keep track of mouse locations.
/// ///
/// All of the parameters must not be null. /// All of the parameters must not be null.
...@@ -129,8 +132,13 @@ class MouseTracker { ...@@ -129,8 +132,13 @@ class MouseTracker {
} }
void _scheduleMousePositionCheck() { void _scheduleMousePositionCheck() {
SchedulerBinding.instance.addPostFrameCallback((Duration _) => collectMousePositions()); // If we're not tracking anything, then there is no point in registering a
SchedulerBinding.instance.scheduleFrame(); // frame callback or scheduling a frame. By definition there are no active
// annotations that need exiting, either.
if (_trackedAnnotations.isNotEmpty) {
SchedulerBinding.instance.addPostFrameCallback((Duration _) => collectMousePositions());
SchedulerBinding.instance.scheduleFrame();
}
} }
// Handler for events coming from the PointerRouter. // Handler for events coming from the PointerRouter.
...@@ -139,15 +147,12 @@ class MouseTracker { ...@@ -139,15 +147,12 @@ class MouseTracker {
return; return;
} }
final int deviceId = event.device; final int deviceId = event.device;
if (_trackedAnnotations.isEmpty) { if (event is PointerAddedEvent) {
// If we're not tracking anything, then there is no point in registering a _addMouseEvent(deviceId, event);
// frame callback or scheduling a frame. By definition there are no active
// annotations that need exiting, either.
_lastMouseEvent.remove(deviceId);
return; return;
} }
if (event is PointerRemovedEvent) { if (event is PointerRemovedEvent) {
_lastMouseEvent.remove(deviceId); _removeMouseEvent(deviceId);
// If the mouse was removed, then we need to schedule one more check to // If the mouse was removed, then we need to schedule one more check to
// exit any annotations that were active. // exit any annotations that were active.
_scheduleMousePositionCheck(); _scheduleMousePositionCheck();
...@@ -155,10 +160,10 @@ class MouseTracker { ...@@ -155,10 +160,10 @@ class MouseTracker {
if (event is PointerMoveEvent || event is PointerHoverEvent || event is PointerDownEvent) { if (event is PointerMoveEvent || event is PointerHoverEvent || event is PointerDownEvent) {
if (!_lastMouseEvent.containsKey(deviceId) || _lastMouseEvent[deviceId].position != event.position) { if (!_lastMouseEvent.containsKey(deviceId) || _lastMouseEvent[deviceId].position != event.position) {
// Only schedule a frame if we have our first event, or if the // Only schedule a frame if we have our first event, or if the
// location of the mouse has changed. // location of the mouse has changed, and only if there are tracked annotations.
_scheduleMousePositionCheck(); _scheduleMousePositionCheck();
} }
_lastMouseEvent[deviceId] = event; _addMouseEvent(deviceId, event);
} }
} }
} }
...@@ -260,6 +265,22 @@ class MouseTracker { ...@@ -260,6 +265,22 @@ class MouseTracker {
} }
} }
void _addMouseEvent(int deviceId, PointerEvent event) {
final bool wasConnected = mouseIsConnected;
_lastMouseEvent[deviceId] = event;
if (mouseIsConnected != wasConnected) {
notifyListeners();
}
}
void _removeMouseEvent(int deviceId) {
final bool wasConnected = mouseIsConnected;
_lastMouseEvent.remove(deviceId);
if (mouseIsConnected != wasConnected) {
notifyListeners();
}
}
/// The most recent mouse event observed for each mouse device ID observed. /// The most recent mouse event observed for each mouse device ID observed.
/// ///
/// May be null if no mouse is connected, or hasn't produced an event yet. /// May be null if no mouse is connected, or hasn't produced an event yet.
......
...@@ -2565,12 +2565,13 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior { ...@@ -2565,12 +2565,13 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior {
MouseTrackerAnnotation get hoverAnnotation => _hoverAnnotation; MouseTrackerAnnotation get hoverAnnotation => _hoverAnnotation;
void _updateAnnotations() { void _updateAnnotations() {
assert(_onPointerEnter != _hoverAnnotation.onEnter || _onPointerHover != _hoverAnnotation.onHover || _onPointerExit != _hoverAnnotation.onExit, bool changed = false;
"Shouldn't call _updateAnnotations if nothing has changed.");
if (_hoverAnnotation != null && attached) { if (_hoverAnnotation != null && attached) {
RendererBinding.instance.mouseTracker.detachAnnotation(_hoverAnnotation); RendererBinding.instance.mouseTracker.detachAnnotation(_hoverAnnotation);
changed = true;
} }
if (_onPointerEnter != null || _onPointerHover != null || _onPointerExit != null) { if (RendererBinding.instance.mouseTracker.mouseIsConnected &&
(_onPointerEnter != null || _onPointerHover != null || _onPointerExit != null)) {
_hoverAnnotation = MouseTrackerAnnotation( _hoverAnnotation = MouseTrackerAnnotation(
onEnter: _onPointerEnter, onEnter: _onPointerEnter,
onHover: _onPointerHover, onHover: _onPointerHover,
...@@ -2578,18 +2579,21 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior { ...@@ -2578,18 +2579,21 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior {
); );
if (attached) { if (attached) {
RendererBinding.instance.mouseTracker.attachAnnotation(_hoverAnnotation); RendererBinding.instance.mouseTracker.attachAnnotation(_hoverAnnotation);
changed = true;
} }
} else { } else {
_hoverAnnotation = null; _hoverAnnotation = null;
} }
// Needs to paint in any case, in order to insert/remove the annotation if (changed) {
// layer associated with the updated _hoverAnnotation. markNeedsPaint();
markNeedsPaint(); }
} }
@override @override
void attach(PipelineOwner owner) { void attach(PipelineOwner owner) {
super.attach(owner); super.attach(owner);
// Add a listener to listen for changes in mouseIsConnected.
RendererBinding.instance.mouseTracker.addListener(_updateAnnotations);
if (_hoverAnnotation != null) { if (_hoverAnnotation != null) {
RendererBinding.instance.mouseTracker.attachAnnotation(_hoverAnnotation); RendererBinding.instance.mouseTracker.attachAnnotation(_hoverAnnotation);
} }
...@@ -2600,6 +2604,7 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior { ...@@ -2600,6 +2604,7 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior {
if (_hoverAnnotation != null) { if (_hoverAnnotation != null) {
RendererBinding.instance.mouseTracker.detachAnnotation(_hoverAnnotation); RendererBinding.instance.mouseTracker.detachAnnotation(_hoverAnnotation);
} }
RendererBinding.instance.mouseTracker.removeListener(_updateAnnotations);
super.detach(); super.detach();
} }
......
...@@ -539,12 +539,13 @@ abstract class WidgetController { ...@@ -539,12 +539,13 @@ abstract class WidgetController {
/// You can use [startGesture] instead if your gesture begins with a down /// You can use [startGesture] instead if your gesture begins with a down
/// event. /// event.
Future<TestGesture> createGesture({int pointer, PointerDeviceKind kind = PointerDeviceKind.touch}) async { Future<TestGesture> createGesture({int pointer, PointerDeviceKind kind = PointerDeviceKind.touch}) async {
return TestGesture( final TestGesture gesture = TestGesture(
hitTester: hitTestOnBinding, hitTester: hitTestOnBinding,
dispatcher: sendEventToBinding, dispatcher: sendEventToBinding,
kind: kind, kind: kind,
pointer: pointer ?? _getNextPointer(), pointer: pointer ?? _getNextPointer(),
); );
return gesture;
} }
/// Creates a gesture with an initial down gesture at a particular point, and /// Creates a gesture with an initial down gesture at a particular point, and
......
...@@ -22,9 +22,31 @@ class TestPointer { ...@@ -22,9 +22,31 @@ class TestPointer {
/// ///
/// Multiple [TestPointer]s created with the same pointer identifier will /// Multiple [TestPointer]s created with the same pointer identifier will
/// interfere with each other if they are used in parallel. /// interfere with each other if they are used in parallel.
TestPointer([this.pointer = 1, this.kind = PointerDeviceKind.touch]) TestPointer([
: assert(kind != null), this.pointer = 1,
assert(pointer != null); this.kind = PointerDeviceKind.touch,
this._device,
]) : assert(kind != null),
assert(pointer != null) {
switch (kind) {
case PointerDeviceKind.mouse:
_device ??= 1;
break;
case PointerDeviceKind.stylus:
case PointerDeviceKind.invertedStylus:
case PointerDeviceKind.touch:
case PointerDeviceKind.unknown:
_device ??= 0;
break;
}
}
/// The device identifier used for events generated by this object.
///
/// Set when the object is constructed. Defaults to 1 if the [kind] is
/// [PointerDeviceKind.mouse], and 0 otherwise.
int get device => _device;
int _device;
/// The pointer identifier used for events generated by this object. /// The pointer identifier used for events generated by this object.
/// ///
...@@ -63,7 +85,8 @@ class TestPointer { ...@@ -63,7 +85,8 @@ class TestPointer {
assert(isDown); assert(isDown);
_isDown = false; _isDown = false;
break; break;
default: break; default:
break;
} }
return isDown; return isDown;
} }
...@@ -72,7 +95,7 @@ class TestPointer { ...@@ -72,7 +95,7 @@ class TestPointer {
/// ///
/// By default, the time stamp on the event is [Duration.zero]. You can give a /// By default, the time stamp on the event is [Duration.zero]. You can give a
/// specific time stamp by passing the `timeStamp` argument. /// specific time stamp by passing the `timeStamp` argument.
PointerDownEvent down(Offset newLocation, {Duration timeStamp = Duration.zero}) { PointerDownEvent down(Offset newLocation, { Duration timeStamp = Duration.zero }) {
assert(!isDown); assert(!isDown);
_isDown = true; _isDown = true;
_location = newLocation; _location = newLocation;
...@@ -91,7 +114,7 @@ class TestPointer { ...@@ -91,7 +114,7 @@ class TestPointer {
/// ///
/// [isDown] must be true when this is called, since move events can only /// [isDown] must be true when this is called, since move events can only
/// be generated when the pointer is down. /// be generated when the pointer is down.
PointerMoveEvent move(Offset newLocation, {Duration timeStamp = Duration.zero}) { PointerMoveEvent move(Offset newLocation, { Duration timeStamp = Duration.zero }) {
assert( assert(
isDown, isDown,
'Move events can only be generated when the pointer is down. To ' 'Move events can only be generated when the pointer is down. To '
...@@ -114,7 +137,7 @@ class TestPointer { ...@@ -114,7 +137,7 @@ class TestPointer {
/// specific time stamp by passing the `timeStamp` argument. /// specific time stamp by passing the `timeStamp` argument.
/// ///
/// The object is no longer usable after this method has been called. /// The object is no longer usable after this method has been called.
PointerUpEvent up({Duration timeStamp = Duration.zero}) { PointerUpEvent up({ Duration timeStamp = Duration.zero }) {
assert(isDown); assert(isDown);
_isDown = false; _isDown = false;
return PointerUpEvent( return PointerUpEvent(
...@@ -131,7 +154,7 @@ class TestPointer { ...@@ -131,7 +154,7 @@ class TestPointer {
/// specific time stamp by passing the `timeStamp` argument. /// specific time stamp by passing the `timeStamp` argument.
/// ///
/// The object is no longer usable after this method has been called. /// The object is no longer usable after this method has been called.
PointerCancelEvent cancel({Duration timeStamp = Duration.zero}) { PointerCancelEvent cancel({ Duration timeStamp = Duration.zero }) {
assert(isDown); assert(isDown);
_isDown = false; _isDown = false;
return PointerCancelEvent( return PointerCancelEvent(
...@@ -142,6 +165,43 @@ class TestPointer { ...@@ -142,6 +165,43 @@ class TestPointer {
); );
} }
/// Create a [PointerAddedEvent] with the [PointerDeviceKind] the pointer was
/// created with.
///
/// By default, the time stamp on the event is [Duration.zero]. You can give a
/// specific time stamp by passing the `timeStamp` argument.
///
/// [isDown] must be false, since hover events can't be sent when the pointer
/// is up.
PointerAddedEvent addPointer({
Duration timeStamp = Duration.zero,
}) {
assert(timeStamp != null);
return PointerAddedEvent(
timeStamp: timeStamp,
kind: kind,
device: _device,
);
}
/// Create a [PointerRemovedEvent] with the kind the pointer was created with.
///
/// By default, the time stamp on the event is [Duration.zero]. You can give a
/// specific time stamp by passing the `timeStamp` argument.
///
/// [isDown] must be false, since hover events can't be sent when the pointer
/// is up.
PointerRemovedEvent removePointer({
Duration timeStamp = Duration.zero,
}) {
assert(timeStamp != null);
return PointerRemovedEvent(
timeStamp: timeStamp,
kind: kind,
device: _device,
);
}
/// Create a [PointerHoverEvent] to the given location. /// Create a [PointerHoverEvent] to the given location.
/// ///
/// By default, the time stamp on the event is [Duration.zero]. You can give a /// By default, the time stamp on the event is [Duration.zero]. You can give a
...@@ -160,12 +220,13 @@ class TestPointer { ...@@ -160,12 +220,13 @@ class TestPointer {
'Hover events can only be generated when the pointer is up. To ' 'Hover events can only be generated when the pointer is up. To '
'simulate movement when the pointer is down, use move() instead.'); 'simulate movement when the pointer is down, use move() instead.');
assert(kind != PointerDeviceKind.touch, "Touch pointers can't generate hover events"); assert(kind != PointerDeviceKind.touch, "Touch pointers can't generate hover events");
final Offset delta = location != null ? newLocation - location : Offset.zero; final Offset delta = location != null ? newLocation - location : Offset.zero;
_location = newLocation; _location = newLocation;
return PointerHoverEvent( return PointerHoverEvent(
timeStamp: timeStamp, timeStamp: timeStamp,
kind: kind, kind: kind,
position: newLocation, position: newLocation,
device: _device,
delta: delta, delta: delta,
); );
} }
...@@ -267,12 +328,26 @@ class TestGesture { ...@@ -267,12 +328,26 @@ class TestGesture {
}); });
} }
/// In a test, send a pointer add event for this pointer.
Future<void> addPointer({ Duration timeStamp = Duration.zero }) {
return TestAsyncUtils.guard<void>(() {
return _dispatcher(_pointer.addPointer(timeStamp: timeStamp), null);
});
}
/// In a test, send a pointer remove event for this pointer.
Future<void> removePointer({ Duration timeStamp = Duration.zero }) {
return TestAsyncUtils.guard<void>(() {
return _dispatcher(_pointer.removePointer(timeStamp: timeStamp), null);
});
}
/// Send a move event moving the pointer by the given offset. /// Send a move event moving the pointer by the given offset.
/// ///
/// If the pointer is down, then a move event is dispatched. If the pointer is /// If the pointer is down, then a move event is dispatched. If the pointer is
/// up, then a hover event is dispatched. Touch devices are not able to send /// up, then a hover event is dispatched. Touch devices are not able to send
/// hover events. /// hover events.
Future<void> moveBy(Offset offset, {Duration timeStamp = Duration.zero}) { Future<void> moveBy(Offset offset, { Duration timeStamp = Duration.zero }) {
return moveTo(_pointer.location + offset, timeStamp: timeStamp); return moveTo(_pointer.location + offset, timeStamp: timeStamp);
} }
...@@ -281,7 +356,7 @@ class TestGesture { ...@@ -281,7 +356,7 @@ class TestGesture {
/// If the pointer is down, then a move event is dispatched. If the pointer is /// If the pointer is down, then a move event is dispatched. If the pointer is
/// up, then a hover event is dispatched. Touch devices are not able to send /// up, then a hover event is dispatched. Touch devices are not able to send
/// hover events. /// hover events.
Future<void> moveTo(Offset location, {Duration timeStamp = Duration.zero}) { Future<void> moveTo(Offset location, { Duration timeStamp = Duration.zero }) {
return TestAsyncUtils.guard<void>(() { return TestAsyncUtils.guard<void>(() {
if (_pointer._isDown) { if (_pointer._isDown) {
assert(_result != null, assert(_result != null,
......
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