blob: b74224eba17e8025b10df73cf233577aedfcf686 [file] [log] [blame]
// Copyright 2014 The Flutter 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 'dart:ui' as ui;
import 'dart:ui' show PointerChange;
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'mouse_tracker_test_utils.dart';
typedef MethodCallHandler = Future<dynamic> Function(MethodCall call);
typedef SimpleAnnotationFinder = Iterable<HitTestTarget> Function(Offset offset);
void main() {
final TestMouseTrackerFlutterBinding _binding = TestMouseTrackerFlutterBinding();
MethodCallHandler? _methodCallHandler;
// Only one of `logCursors` and `cursorHandler` should be specified.
void _setUpMouseTracker({
required SimpleAnnotationFinder annotationFinder,
List<_CursorUpdateDetails>? logCursors,
MethodCallHandler? cursorHandler,
}) {
assert(logCursors == null || cursorHandler == null);
_methodCallHandler = logCursors != null
? (MethodCall call) async {
logCursors.add(_CursorUpdateDetails.wrap(call));
return;
}
: cursorHandler;
_binding.setHitTest((BoxHitTestResult result, Offset position) {
for (final HitTestTarget target in annotationFinder(position)) {
result.addWithRawTransform(
transform: Matrix4.identity(),
position: position,
hitTest: (BoxHitTestResult result, Offset position) {
result.add(HitTestEntry(target));
return true;
},
);
}
return true;
});
}
void dispatchRemoveDevice([int device = 0]) {
ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.remove, Offset.zero, device: device),
]));
}
setUp(() {
_binding.postFrameCallbacks.clear();
_binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.mouseCursor, (MethodCall call) async {
if (_methodCallHandler != null)
return _methodCallHandler!(call);
});
});
tearDown(() {
_binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.mouseCursor, null);
});
test('Should work on platforms that does not support mouse cursor', () async {
const TestAnnotationTarget annotation = TestAnnotationTarget(cursor: SystemMouseCursors.grabbing);
_setUpMouseTracker(
annotationFinder: (Offset position) => <TestAnnotationTarget>[annotation],
cursorHandler: (MethodCall call) async {
return null;
},
);
ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, Offset.zero),
]));
addTearDown(dispatchRemoveDevice);
// Passes if no errors are thrown
});
test('pointer is added and removed out of any annotations', () {
final List<_CursorUpdateDetails> logCursors = <_CursorUpdateDetails>[];
TestAnnotationTarget? annotation;
_setUpMouseTracker(
annotationFinder: (Offset position) => <TestAnnotationTarget>[if (annotation != null) annotation],
logCursors: logCursors,
);
// Pointer is added outside of the annotation.
ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, Offset.zero),
]));
expect(logCursors, <_CursorUpdateDetails>[
_CursorUpdateDetails.activateSystemCursor(device: 0, kind: SystemMouseCursors.basic.kind),
]);
logCursors.clear();
// Pointer moves into the annotation
annotation = const TestAnnotationTarget(cursor: SystemMouseCursors.grabbing);
ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, const Offset(5.0, 0.0)),
]));
expect(logCursors, <_CursorUpdateDetails>[
_CursorUpdateDetails.activateSystemCursor(device: 0, kind: SystemMouseCursors.grabbing.kind),
]);
logCursors.clear();
// Pointer moves within the annotation
annotation = const TestAnnotationTarget(cursor: SystemMouseCursors.grabbing);
ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, const Offset(10.0, 0.0)),
]));
expect(logCursors, <_CursorUpdateDetails>[]);
logCursors.clear();
// Pointer moves out of the annotation
annotation = null;
ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, Offset.zero),
]));
expect(logCursors, <_CursorUpdateDetails>[
_CursorUpdateDetails.activateSystemCursor(device: 0, kind: SystemMouseCursors.basic.kind),
]);
logCursors.clear();
// Pointer is removed outside of the annotation.
ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.remove, Offset.zero),
]));
expect(logCursors, const <_CursorUpdateDetails>[]);
});
test('pointer is added and removed in an annotation', () {
final List<_CursorUpdateDetails> logCursors = <_CursorUpdateDetails>[];
TestAnnotationTarget? annotation;
_setUpMouseTracker(
annotationFinder: (Offset position) => <TestAnnotationTarget>[if (annotation != null) annotation],
logCursors: logCursors,
);
// Pointer is added in the annotation.
annotation = const TestAnnotationTarget(cursor: SystemMouseCursors.grabbing);
ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, Offset.zero),
]));
expect(logCursors, <_CursorUpdateDetails>[
_CursorUpdateDetails.activateSystemCursor(device: 0, kind: SystemMouseCursors.grabbing.kind),
]);
logCursors.clear();
// Pointer moves out of the annotation
annotation = null;
ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, const Offset(5.0, 0.0)),
]));
expect(logCursors, <_CursorUpdateDetails>[
_CursorUpdateDetails.activateSystemCursor(device: 0, kind: SystemMouseCursors.basic.kind),
]);
logCursors.clear();
// Pointer moves around out of the annotation
annotation = null;
ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, const Offset(10.0, 0.0)),
]));
expect(logCursors, <_CursorUpdateDetails>[]);
logCursors.clear();
// Pointer moves back into the annotation
annotation = const TestAnnotationTarget(cursor: SystemMouseCursors.grabbing);
ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, Offset.zero),
]));
expect(logCursors, <_CursorUpdateDetails>[
_CursorUpdateDetails.activateSystemCursor(device: 0, kind: SystemMouseCursors.grabbing.kind),
]);
logCursors.clear();
// Pointer is removed within the annotation.
ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.remove, Offset.zero),
]));
expect(logCursors, <_CursorUpdateDetails>[]);
});
test('pointer change caused by new frames', () {
final List<_CursorUpdateDetails> logCursors = <_CursorUpdateDetails>[];
TestAnnotationTarget? annotation;
_setUpMouseTracker(
annotationFinder: (Offset position) => <TestAnnotationTarget>[if (annotation != null) annotation],
logCursors: logCursors,
);
// Pointer is added outside of the annotation.
ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, Offset.zero),
]));
expect(logCursors, <_CursorUpdateDetails>[
_CursorUpdateDetails.activateSystemCursor(device: 0, kind: SystemMouseCursors.basic.kind),
]);
logCursors.clear();
// Synthesize a new frame while changing annotation
annotation = const TestAnnotationTarget(cursor: SystemMouseCursors.grabbing);
_binding.scheduleMouseTrackerPostFrameCheck();
_binding.flushPostFrameCallbacks(Duration.zero);
expect(logCursors, <_CursorUpdateDetails>[
_CursorUpdateDetails.activateSystemCursor(device: 0, kind: SystemMouseCursors.grabbing.kind),
]);
logCursors.clear();
// Synthesize a new frame without changing annotation
annotation = const TestAnnotationTarget(cursor: SystemMouseCursors.grabbing);
_binding.scheduleMouseTrackerPostFrameCheck();
expect(logCursors, <_CursorUpdateDetails>[]);
logCursors.clear();
// Pointer is removed outside of the annotation.
ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.remove, Offset.zero),
]));
expect(logCursors, <_CursorUpdateDetails>[]);
});
test('The first annotation with non-deferring cursor is used', () {
final List<_CursorUpdateDetails> logCursors = <_CursorUpdateDetails>[];
late List<TestAnnotationTarget> annotations;
_setUpMouseTracker(
annotationFinder: (Offset position) sync* { yield* annotations; },
logCursors: logCursors,
);
annotations = <TestAnnotationTarget>[
const TestAnnotationTarget(),
const TestAnnotationTarget(cursor: SystemMouseCursors.click),
const TestAnnotationTarget(cursor: SystemMouseCursors.grabbing),
];
ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, Offset.zero),
]));
expect(logCursors, <_CursorUpdateDetails>[
_CursorUpdateDetails.activateSystemCursor(device: 0, kind: SystemMouseCursors.click.kind),
]);
logCursors.clear();
// Remove
ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.remove, const Offset(5.0, 0.0)),
]));
});
test('Annotations with deferring cursors are ignored', () {
final List<_CursorUpdateDetails> logCursors = <_CursorUpdateDetails>[];
late List<TestAnnotationTarget> annotations;
_setUpMouseTracker(
annotationFinder: (Offset position) sync* { yield* annotations; },
logCursors: logCursors,
);
annotations = <TestAnnotationTarget>[
const TestAnnotationTarget(),
const TestAnnotationTarget(),
const TestAnnotationTarget(cursor: SystemMouseCursors.grabbing),
];
ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, Offset.zero),
]));
expect(logCursors, <_CursorUpdateDetails>[
_CursorUpdateDetails.activateSystemCursor(device: 0, kind: SystemMouseCursors.grabbing.kind),
]);
logCursors.clear();
// Remove
ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.remove, const Offset(5.0, 0.0)),
]));
});
test('Finding no annotation is equivalent to specifying default cursor', () {
final List<_CursorUpdateDetails> logCursors = <_CursorUpdateDetails>[];
TestAnnotationTarget? annotation;
_setUpMouseTracker(
annotationFinder: (Offset position) => <TestAnnotationTarget>[if (annotation != null) annotation],
logCursors: logCursors,
);
// Pointer is added outside of the annotation.
ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, Offset.zero),
]));
expect(logCursors, <_CursorUpdateDetails>[
_CursorUpdateDetails.activateSystemCursor(device: 0, kind: SystemMouseCursors.basic.kind),
]);
logCursors.clear();
// Pointer moved to an annotation specified with the default cursor
annotation = const TestAnnotationTarget(cursor: SystemMouseCursors.basic);
ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, const Offset(5.0, 0.0)),
]));
expect(logCursors, <_CursorUpdateDetails>[]);
logCursors.clear();
// Pointer moved to no annotations
annotation = null;
ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, Offset.zero),
]));
expect(logCursors, <_CursorUpdateDetails>[]);
logCursors.clear();
// Remove
ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.remove, Offset.zero),
]));
});
test('Removing a pointer resets it back to the default cursor', () {
final List<_CursorUpdateDetails> logCursors = <_CursorUpdateDetails>[];
TestAnnotationTarget? annotation;
_setUpMouseTracker(
annotationFinder: (Offset position) => <TestAnnotationTarget>[if (annotation != null) annotation],
logCursors: logCursors,
);
// Pointer is added to the annotation, then removed
annotation = const TestAnnotationTarget(cursor: SystemMouseCursors.click);
ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, Offset.zero),
_pointerData(PointerChange.hover, const Offset(5.0, 0.0)),
_pointerData(PointerChange.remove, const Offset(5.0, 0.0)),
]));
logCursors.clear();
// Pointer is added out of the annotation
annotation = null;
ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, Offset.zero),
]));
addTearDown(dispatchRemoveDevice);
expect(logCursors, <_CursorUpdateDetails>[
_CursorUpdateDetails.activateSystemCursor(device: 0, kind: SystemMouseCursors.basic.kind),
]);
logCursors.clear();
});
test('Pointing devices display cursors separately', () {
final List<_CursorUpdateDetails> logCursors = <_CursorUpdateDetails>[];
_setUpMouseTracker(
annotationFinder: (Offset position) sync* {
if (position.dx > 200) {
yield const TestAnnotationTarget(cursor: SystemMouseCursors.forbidden);
} else if (position.dx > 100) {
yield const TestAnnotationTarget(cursor: SystemMouseCursors.click);
} else {}
},
logCursors: logCursors,
);
// Pointers are added outside of the annotation.
ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, Offset.zero, device: 1),
_pointerData(PointerChange.add, Offset.zero, device: 2),
]));
addTearDown(() => dispatchRemoveDevice(1));
addTearDown(() => dispatchRemoveDevice(2));
expect(logCursors, <_CursorUpdateDetails>[
_CursorUpdateDetails.activateSystemCursor(device: 1, kind: SystemMouseCursors.basic.kind),
_CursorUpdateDetails.activateSystemCursor(device: 2, kind: SystemMouseCursors.basic.kind),
]);
logCursors.clear();
// Pointer 1 moved to cursor "click"
ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, const Offset(101.0, 0.0), device: 1),
]));
expect(logCursors, <_CursorUpdateDetails>[
_CursorUpdateDetails.activateSystemCursor(device: 1, kind: SystemMouseCursors.click.kind),
]);
logCursors.clear();
// Pointer 2 moved to cursor "click"
ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, const Offset(102.0, 0.0), device: 2),
]));
expect(logCursors, <_CursorUpdateDetails>[
_CursorUpdateDetails.activateSystemCursor(device: 2, kind: SystemMouseCursors.click.kind),
]);
logCursors.clear();
// Pointer 2 moved to cursor "forbidden"
ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, const Offset(202.0, 0.0), device: 2),
]));
expect(logCursors, <_CursorUpdateDetails>[
_CursorUpdateDetails.activateSystemCursor(device: 2, kind: SystemMouseCursors.forbidden.kind),
]);
logCursors.clear();
});
}
ui.PointerData _pointerData(
PointerChange change,
Offset logicalPosition, {
int device = 0,
PointerDeviceKind kind = PointerDeviceKind.mouse,
}) {
return ui.PointerData(
change: change,
physicalX: logicalPosition.dx * ui.window.devicePixelRatio,
physicalY: logicalPosition.dy * ui.window.devicePixelRatio,
kind: kind,
device: device,
);
}
class _CursorUpdateDetails extends MethodCall {
const _CursorUpdateDetails(String method, Map<String, dynamic> arguments)
: assert(arguments != null),
super(method, arguments);
_CursorUpdateDetails.wrap(MethodCall call)
: super(call.method, Map<String, dynamic>.from(call.arguments as Map<dynamic, dynamic>));
_CursorUpdateDetails.activateSystemCursor({
required int device,
required String kind,
}) : this('activateSystemCursor', <String, dynamic>{'device': device, 'kind': kind});
@override
Map<String, dynamic> get arguments => super.arguments as Map<String, dynamic>;
@override
bool operator ==(dynamic other) {
if (identical(other, this))
return true;
if (other.runtimeType != runtimeType)
return false;
return other is _CursorUpdateDetails
&& other.method == method
&& other.arguments.length == arguments.length
&& other.arguments.entries.every(
(MapEntry<String, dynamic> entry) =>
arguments.containsKey(entry.key) && arguments[entry.key] == entry.value,
);
}
@override
int get hashCode => hashValues(method, arguments);
@override
String toString() {
return '_CursorUpdateDetails(method: $method, arguments: $arguments)';
}
}