mouse_tracker_cursor_test.dart 17.3 KB
Newer Older
1 2 3 4 5 6 7 8
// 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';
9
import 'package:flutter/rendering.dart';
10
import 'package:flutter/services.dart';
11
import 'package:flutter_test/flutter_test.dart';
12

13
import 'mouse_tracker_test_utils.dart';
14 15

typedef MethodCallHandler = Future<dynamic> Function(MethodCall call);
16
typedef SimpleAnnotationFinder = Iterable<HitTestTarget> Function(Offset offset);
17

18
void main() {
19 20
  final TestMouseTrackerFlutterBinding _binding = TestMouseTrackerFlutterBinding();
  MethodCallHandler? _methodCallHandler;
21 22 23

  // Only one of `logCursors` and `cursorHandler` should be specified.
  void _setUpMouseTracker({
24 25 26
    required SimpleAnnotationFinder annotationFinder,
    List<_CursorUpdateDetails>? logCursors,
    MethodCallHandler? cursorHandler,
27 28 29 30 31 32 33 34
  }) {
    assert(logCursors == null || cursorHandler == null);
    _methodCallHandler = logCursors != null
      ? (MethodCall call) async {
        logCursors.add(_CursorUpdateDetails.wrap(call));
        return;
      }
      : cursorHandler;
35 36 37 38 39

    _binding.setHitTest((BoxHitTestResult result, Offset position) {
      for (final HitTestTarget target in annotationFinder(position)) {
        result.addWithRawTransform(
          transform: Matrix4.identity(),
40
          position: position,
41 42 43 44 45 46 47 48 49 50 51
          hitTest: (BoxHitTestResult result, Offset position) {
            result.add(HitTestEntry(target));
            return true;
          },
        );
      }
      return true;
    });
  }

  void dispatchRemoveDevice([int device = 0]) {
52
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
53
      _pointerData(PointerChange.remove, Offset.zero, device: device),
54
    ]));
55 56 57 58
  }

  setUp(() {
    _binding.postFrameCallbacks.clear();
59
    _binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.mouseCursor, (MethodCall call) async {
60
      if (_methodCallHandler != null)
61
        return _methodCallHandler!(call);
62 63 64 65
    });
  });

  tearDown(() {
66
    _binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.mouseCursor, null);
67 68 69
  });

  test('Should work on platforms that does not support mouse cursor', () async {
70
    const TestAnnotationTarget annotation = TestAnnotationTarget(cursor: SystemMouseCursors.grabbing);
71 72

    _setUpMouseTracker(
73
      annotationFinder: (Offset position) => <TestAnnotationTarget>[annotation],
74 75 76 77 78
      cursorHandler: (MethodCall call) async {
        return null;
      },
    );

79
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
80
      _pointerData(PointerChange.add, Offset.zero),
81
    ]));
82
    addTearDown(dispatchRemoveDevice);
83 84 85 86 87 88

    // Passes if no errors are thrown
  });

  test('pointer is added and removed out of any annotations', () {
    final List<_CursorUpdateDetails> logCursors = <_CursorUpdateDetails>[];
89
    TestAnnotationTarget? annotation;
90
    _setUpMouseTracker(
91
      annotationFinder: (Offset position) => <TestAnnotationTarget>[if (annotation != null) annotation],
92 93 94 95
      logCursors: logCursors,
    );

    // Pointer is added outside of the annotation.
96
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
97
      _pointerData(PointerChange.add, Offset.zero),
98 99 100 101 102 103 104 105
    ]));

    expect(logCursors, <_CursorUpdateDetails>[
      _CursorUpdateDetails.activateSystemCursor(device: 0, kind: SystemMouseCursors.basic.kind),
    ]);
    logCursors.clear();

    // Pointer moves into the annotation
106
    annotation = const TestAnnotationTarget(cursor: SystemMouseCursors.grabbing);
107
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
108 109 110 111 112 113 114 115 116
      _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
117
    annotation = const TestAnnotationTarget(cursor: SystemMouseCursors.grabbing);
118
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
119 120 121
      _pointerData(PointerChange.hover, const Offset(10.0, 0.0)),
    ]));

122
    expect(logCursors, <_CursorUpdateDetails>[]);
123 124 125 126
    logCursors.clear();

    // Pointer moves out of the annotation
    annotation = null;
127
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
128
      _pointerData(PointerChange.hover, Offset.zero),
129 130 131 132 133 134 135 136
    ]));

    expect(logCursors, <_CursorUpdateDetails>[
      _CursorUpdateDetails.activateSystemCursor(device: 0, kind: SystemMouseCursors.basic.kind),
    ]);
    logCursors.clear();

    // Pointer is removed outside of the annotation.
137
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
138
      _pointerData(PointerChange.remove, Offset.zero),
139 140
    ]));

141
    expect(logCursors, const <_CursorUpdateDetails>[]);
142 143 144 145
  });

  test('pointer is added and removed in an annotation', () {
    final List<_CursorUpdateDetails> logCursors = <_CursorUpdateDetails>[];
146
    TestAnnotationTarget? annotation;
147
    _setUpMouseTracker(
148
      annotationFinder: (Offset position) => <TestAnnotationTarget>[if (annotation != null) annotation],
149 150 151 152
      logCursors: logCursors,
    );

    // Pointer is added in the annotation.
153
    annotation = const TestAnnotationTarget(cursor: SystemMouseCursors.grabbing);
154
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
155
      _pointerData(PointerChange.add, Offset.zero),
156 157 158 159 160 161 162 163 164
    ]));

    expect(logCursors, <_CursorUpdateDetails>[
      _CursorUpdateDetails.activateSystemCursor(device: 0, kind: SystemMouseCursors.grabbing.kind),
    ]);
    logCursors.clear();

    // Pointer moves out of the annotation
    annotation = null;
165
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
166 167 168 169 170 171 172 173 174 175
      _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;
176
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
177 178 179
      _pointerData(PointerChange.hover, const Offset(10.0, 0.0)),
    ]));

180
    expect(logCursors, <_CursorUpdateDetails>[]);
181 182 183
    logCursors.clear();

    // Pointer moves back into the annotation
184
    annotation = const TestAnnotationTarget(cursor: SystemMouseCursors.grabbing);
185
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
186
      _pointerData(PointerChange.hover, Offset.zero),
187 188 189 190 191 192 193 194
    ]));

    expect(logCursors, <_CursorUpdateDetails>[
      _CursorUpdateDetails.activateSystemCursor(device: 0, kind: SystemMouseCursors.grabbing.kind),
    ]);
    logCursors.clear();

    // Pointer is removed within the annotation.
195
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
196
      _pointerData(PointerChange.remove, Offset.zero),
197 198
    ]));

199
    expect(logCursors, <_CursorUpdateDetails>[]);
200 201 202 203
  });

  test('pointer change caused by new frames', () {
    final List<_CursorUpdateDetails> logCursors = <_CursorUpdateDetails>[];
204
    TestAnnotationTarget? annotation;
205
    _setUpMouseTracker(
206
      annotationFinder: (Offset position) => <TestAnnotationTarget>[if (annotation != null) annotation],
207 208 209 210
      logCursors: logCursors,
    );

    // Pointer is added outside of the annotation.
211
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
212
      _pointerData(PointerChange.add, Offset.zero),
213 214 215 216 217 218 219 220
    ]));

    expect(logCursors, <_CursorUpdateDetails>[
      _CursorUpdateDetails.activateSystemCursor(device: 0, kind: SystemMouseCursors.basic.kind),
    ]);
    logCursors.clear();

    // Synthesize a new frame while changing annotation
221
    annotation = const TestAnnotationTarget(cursor: SystemMouseCursors.grabbing);
222 223 224 225 226 227 228 229 230
    _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
231
    annotation = const TestAnnotationTarget(cursor: SystemMouseCursors.grabbing);
232 233
    _binding.scheduleMouseTrackerPostFrameCheck();

234
    expect(logCursors, <_CursorUpdateDetails>[]);
235 236 237
    logCursors.clear();

    // Pointer is removed outside of the annotation.
238
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
239
      _pointerData(PointerChange.remove, Offset.zero),
240 241
    ]));

242
    expect(logCursors, <_CursorUpdateDetails>[]);
243 244
  });

245
  test('The first annotation with non-deferring cursor is used', () {
246
    final List<_CursorUpdateDetails> logCursors = <_CursorUpdateDetails>[];
247
    late List<TestAnnotationTarget> annotations;
248 249 250 251 252
    _setUpMouseTracker(
      annotationFinder: (Offset position) sync* { yield* annotations; },
      logCursors: logCursors,
    );

253 254 255 256
    annotations = <TestAnnotationTarget>[
      const TestAnnotationTarget(cursor: MouseCursor.defer),
      const TestAnnotationTarget(cursor: SystemMouseCursors.click),
      const TestAnnotationTarget(cursor: SystemMouseCursors.grabbing),
257
    ];
258
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
259
      _pointerData(PointerChange.add, Offset.zero),
260 261 262 263 264 265 266 267
    ]));

    expect(logCursors, <_CursorUpdateDetails>[
      _CursorUpdateDetails.activateSystemCursor(device: 0, kind: SystemMouseCursors.click.kind),
    ]);
    logCursors.clear();

    // Remove
268
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
269 270 271 272
      _pointerData(PointerChange.remove, const Offset(5.0, 0.0)),
    ]));
  });

273
  test('Annotations with deferring cursors are ignored', () {
274
    final List<_CursorUpdateDetails> logCursors = <_CursorUpdateDetails>[];
275
    late List<TestAnnotationTarget> annotations;
276 277 278 279 280
    _setUpMouseTracker(
      annotationFinder: (Offset position) sync* { yield* annotations; },
      logCursors: logCursors,
    );

281 282 283 284
    annotations = <TestAnnotationTarget>[
      const TestAnnotationTarget(cursor: MouseCursor.defer),
      const TestAnnotationTarget(cursor: MouseCursor.defer),
      const TestAnnotationTarget(cursor: SystemMouseCursors.grabbing),
285
    ];
286
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
287
      _pointerData(PointerChange.add, Offset.zero),
288 289 290 291 292 293 294 295
    ]));

    expect(logCursors, <_CursorUpdateDetails>[
      _CursorUpdateDetails.activateSystemCursor(device: 0, kind: SystemMouseCursors.grabbing.kind),
    ]);
    logCursors.clear();

    // Remove
296
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
297 298 299 300 301 302
      _pointerData(PointerChange.remove, const Offset(5.0, 0.0)),
    ]));
  });

  test('Finding no annotation is equivalent to specifying default cursor', () {
    final List<_CursorUpdateDetails> logCursors = <_CursorUpdateDetails>[];
303
    TestAnnotationTarget? annotation;
304
    _setUpMouseTracker(
305
      annotationFinder: (Offset position) => <TestAnnotationTarget>[if (annotation != null) annotation],
306 307 308 309
      logCursors: logCursors,
    );

    // Pointer is added outside of the annotation.
310
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
311
      _pointerData(PointerChange.add, Offset.zero),
312 313 314 315 316 317 318 319
    ]));

    expect(logCursors, <_CursorUpdateDetails>[
      _CursorUpdateDetails.activateSystemCursor(device: 0, kind: SystemMouseCursors.basic.kind),
    ]);
    logCursors.clear();

    // Pointer moved to an annotation specified with the default cursor
320
    annotation = const TestAnnotationTarget(cursor: SystemMouseCursors.basic);
321
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
322 323 324
      _pointerData(PointerChange.hover, const Offset(5.0, 0.0)),
    ]));

325
    expect(logCursors, <_CursorUpdateDetails>[]);
326 327 328 329
    logCursors.clear();

    // Pointer moved to no annotations
    annotation = null;
330
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
331
      _pointerData(PointerChange.hover, Offset.zero),
332 333
    ]));

334
    expect(logCursors, <_CursorUpdateDetails>[]);
335 336 337
    logCursors.clear();

    // Remove
338
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
339
      _pointerData(PointerChange.remove, Offset.zero),
340 341 342 343 344
    ]));
  });

  test('Removing a pointer resets it back to the default cursor', () {
    final List<_CursorUpdateDetails> logCursors = <_CursorUpdateDetails>[];
345
    TestAnnotationTarget? annotation;
346
    _setUpMouseTracker(
347
      annotationFinder: (Offset position) => <TestAnnotationTarget>[if (annotation != null) annotation],
348 349 350 351
      logCursors: logCursors,
    );

    // Pointer is added to the annotation, then removed
352
    annotation = const TestAnnotationTarget(cursor: SystemMouseCursors.click);
353
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
354
      _pointerData(PointerChange.add, Offset.zero),
355 356 357 358 359 360 361 362
      _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;
363
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
364
      _pointerData(PointerChange.add, Offset.zero),
365
    ]));
366
    addTearDown(dispatchRemoveDevice);
367 368 369 370 371 372 373 374 375 376 377 378

    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) {
379
          yield const TestAnnotationTarget(cursor: SystemMouseCursors.forbidden);
380
        } else if (position.dx > 100) {
381
          yield const TestAnnotationTarget(cursor: SystemMouseCursors.click);
382 383 384 385 386 387
        } else {}
      },
      logCursors: logCursors,
    );

    // Pointers are added outside of the annotation.
388
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
389 390
      _pointerData(PointerChange.add, Offset.zero, device: 1),
      _pointerData(PointerChange.add, Offset.zero, device: 2),
391
    ]));
392 393
    addTearDown(() => dispatchRemoveDevice(1));
    addTearDown(() => dispatchRemoveDevice(2));
394 395 396 397 398 399 400 401

    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"
402
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
403 404 405 406 407 408 409 410 411
      _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"
412
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
413 414 415 416 417 418 419 420 421
      _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"
422
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455
      _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>));

456 457 458 459
  _CursorUpdateDetails.activateSystemCursor({
    required int device,
    required String kind,
  }) : this('activateSystemCursor', <String, dynamic>{'device': device, 'kind': kind});
460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485
  @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)';
  }
}