| // 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(cursor: MouseCursor.defer), |
| 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(cursor: MouseCursor.defer), |
| const TestAnnotationTarget(cursor: MouseCursor.defer), |
| 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)'; |
| } |
| } |