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