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

  setUp(() {
    _binding.postFrameCallbacks.clear();
    SystemChannels.mouseCursor.setMockMethodCallHandler((MethodCall call) async {
      if (_methodCallHandler != null)
61
        return _methodCallHandler!(call);
62 63 64 65 66 67 68 69
    });
  });

  tearDown(() {
    SystemChannels.mouseCursor.setMockMethodCallHandler(null);
  });

  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 122 123 124 125 126 127
      _pointerData(PointerChange.hover, const Offset(10.0, 0.0)),
    ]));

    expect(logCursors, <_CursorUpdateDetails>[
    ]);
    logCursors.clear();

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

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

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

    expect(logCursors, const <_CursorUpdateDetails>[
    ]);
  });

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

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

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

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

    expect(logCursors, <_CursorUpdateDetails>[
    ]);
    logCursors.clear();

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

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

    // Pointer is removed within the annotation.
198
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
199
      _pointerData(PointerChange.remove, Offset.zero),
200 201 202 203 204 205 206 207
    ]));

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

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

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

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

    // Synthesize a new frame while changing annotation
225
    annotation = const TestAnnotationTarget(cursor: SystemMouseCursors.grabbing);
226 227 228 229 230 231 232 233 234
    _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
235
    annotation = const TestAnnotationTarget(cursor: SystemMouseCursors.grabbing);
236 237 238 239 240 241 242
    _binding.scheduleMouseTrackerPostFrameCheck();

    expect(logCursors, <_CursorUpdateDetails>[
    ]);
    logCursors.clear();

    // Pointer is removed outside of the annotation.
243
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
244
      _pointerData(PointerChange.remove, Offset.zero),
245 246 247 248 249 250
    ]));

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

251
  test('The first annotation with non-deferring cursor is used', () {
252
    final List<_CursorUpdateDetails> logCursors = <_CursorUpdateDetails>[];
253
    late List<TestAnnotationTarget> annotations;
254 255 256 257 258
    _setUpMouseTracker(
      annotationFinder: (Offset position) sync* { yield* annotations; },
      logCursors: logCursors,
    );

259 260 261 262
    annotations = <TestAnnotationTarget>[
      const TestAnnotationTarget(cursor: MouseCursor.defer),
      const TestAnnotationTarget(cursor: SystemMouseCursors.click),
      const TestAnnotationTarget(cursor: SystemMouseCursors.grabbing),
263
    ];
264
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
265
      _pointerData(PointerChange.add, Offset.zero),
266 267 268 269 270 271 272 273
    ]));

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

    // Remove
274
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
275 276 277 278
      _pointerData(PointerChange.remove, const Offset(5.0, 0.0)),
    ]));
  });

279
  test('Annotations with deferring cursors are ignored', () {
280
    final List<_CursorUpdateDetails> logCursors = <_CursorUpdateDetails>[];
281
    late List<TestAnnotationTarget> annotations;
282 283 284 285 286
    _setUpMouseTracker(
      annotationFinder: (Offset position) sync* { yield* annotations; },
      logCursors: logCursors,
    );

287 288 289 290
    annotations = <TestAnnotationTarget>[
      const TestAnnotationTarget(cursor: MouseCursor.defer),
      const TestAnnotationTarget(cursor: MouseCursor.defer),
      const TestAnnotationTarget(cursor: SystemMouseCursors.grabbing),
291
    ];
292
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
293
      _pointerData(PointerChange.add, Offset.zero),
294 295 296 297 298 299 300 301
    ]));

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

    // Remove
302
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
303 304 305 306 307 308
      _pointerData(PointerChange.remove, const Offset(5.0, 0.0)),
    ]));
  });

  test('Finding no annotation is equivalent to specifying default cursor', () {
    final List<_CursorUpdateDetails> logCursors = <_CursorUpdateDetails>[];
309
    TestAnnotationTarget? annotation;
310
    _setUpMouseTracker(
311
      annotationFinder: (Offset position) => <TestAnnotationTarget>[if (annotation != null) annotation],
312 313 314 315
      logCursors: logCursors,
    );

    // Pointer is added outside of the annotation.
316
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
317
      _pointerData(PointerChange.add, Offset.zero),
318 319 320 321 322 323 324 325
    ]));

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

    // Pointer moved to an annotation specified with the default cursor
326
    annotation = const TestAnnotationTarget(cursor: SystemMouseCursors.basic);
327
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
328 329 330 331 332 333 334 335 336
      _pointerData(PointerChange.hover, const Offset(5.0, 0.0)),
    ]));

    expect(logCursors, <_CursorUpdateDetails>[
    ]);
    logCursors.clear();

    // Pointer moved to no annotations
    annotation = null;
337
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
338
      _pointerData(PointerChange.hover, Offset.zero),
339 340 341 342 343 344 345
    ]));

    expect(logCursors, <_CursorUpdateDetails>[
    ]);
    logCursors.clear();

    // Remove
346
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
347
      _pointerData(PointerChange.remove, Offset.zero),
348 349 350 351 352
    ]));
  });

  test('Removing a pointer resets it back to the default cursor', () {
    final List<_CursorUpdateDetails> logCursors = <_CursorUpdateDetails>[];
353
    TestAnnotationTarget? annotation;
354
    _setUpMouseTracker(
355
      annotationFinder: (Offset position) => <TestAnnotationTarget>[if (annotation != null) annotation],
356 357 358 359
      logCursors: logCursors,
    );

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

    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) {
387
          yield const TestAnnotationTarget(cursor: SystemMouseCursors.forbidden);
388
        } else if (position.dx > 100) {
389
          yield const TestAnnotationTarget(cursor: SystemMouseCursors.click);
390 391 392 393 394 395
        } else {}
      },
      logCursors: logCursors,
    );

    // Pointers are added outside of the annotation.
396
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
397 398
      _pointerData(PointerChange.add, Offset.zero, device: 1),
      _pointerData(PointerChange.add, Offset.zero, device: 2),
399
    ]));
400 401
    addTearDown(() => dispatchRemoveDevice(1));
    addTearDown(() => dispatchRemoveDevice(2));
402 403 404 405 406 407 408 409

    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"
410
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
411 412 413 414 415 416 417 418 419
      _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"
420
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
421 422 423 424 425 426 427 428 429
      _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"
430
    ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
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 456 457 458 459 460 461 462 463
      _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>));

464 465 466 467
  _CursorUpdateDetails.activateSystemCursor({
    required int device,
    required String kind,
  }) : this('activateSystemCursor', <String, dynamic>{'device': device, 'kind': kind});
468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493
  @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)';
  }
}