mouse_tracker_cursor_test.dart 18.2 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

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

36
    binding.setHitTest((BoxHitTestResult result, Offset position) {
37 38 39
      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
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
53
      _pointerData(PointerChange.remove, Offset.zero, device: device),
54
    ]));
55 56 57
  }

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

  tearDown(() {
68
    binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.mouseCursor, null);
69 70 71
  });

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

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

81
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
82
      _pointerData(PointerChange.add, Offset.zero),
83
    ]));
84
    addTearDown(dispatchRemoveDevice);
85 86 87 88 89 90

    // Passes if no errors are thrown
  });

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

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

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

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

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

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

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

    // Pointer is removed outside of the annotation.
139
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
140
      _pointerData(PointerChange.remove, Offset.zero),
141 142
    ]));

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

  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
    RendererBinding.instance.platformDispatcher.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
    RendererBinding.instance.platformDispatcher.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
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
179 180 181
      _pointerData(PointerChange.hover, const Offset(10.0, 0.0)),
    ]));

182
    expect(logCursors, <_CursorUpdateDetails>[]);
183 184 185
    logCursors.clear();

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

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

    // Pointer is removed within the annotation.
197
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
198
      _pointerData(PointerChange.remove, Offset.zero),
199 200
    ]));

201
    expect(logCursors, <_CursorUpdateDetails>[]);
202 203 204 205
  });

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

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

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

    // Synthesize a new frame while changing annotation
223
    annotation = const TestAnnotationTarget(cursor: SystemMouseCursors.grabbing);
224 225
    binding.scheduleMouseTrackerPostFrameCheck();
    binding.flushPostFrameCallbacks(Duration.zero);
226 227 228 229 230 231 232

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

    // Synthesize a new frame without changing annotation
233
    annotation = const TestAnnotationTarget(cursor: SystemMouseCursors.grabbing);
234
    binding.scheduleMouseTrackerPostFrameCheck();
235

236
    expect(logCursors, <_CursorUpdateDetails>[]);
237 238 239
    logCursors.clear();

    // Pointer is removed outside of the annotation.
240
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
241
      _pointerData(PointerChange.remove, Offset.zero),
242 243
    ]));

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

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

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

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

    // Remove
270
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
271 272 273 274
      _pointerData(PointerChange.remove, const Offset(5.0, 0.0)),
    ]));
  });

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

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

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

    // Remove
298
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
299 300 301 302 303 304
      _pointerData(PointerChange.remove, const Offset(5.0, 0.0)),
    ]));
  });

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

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

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

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

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

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

336
    expect(logCursors, <_CursorUpdateDetails>[]);
337 338 339
    logCursors.clear();

    // Remove
340
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
341
      _pointerData(PointerChange.remove, Offset.zero),
342 343 344 345 346
    ]));
  });

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

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

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

  test('Pointing devices display cursors separately', () {
    final List<_CursorUpdateDetails> logCursors = <_CursorUpdateDetails>[];
378
    setUpMouseTracker(
379 380
      annotationFinder: (Offset position) sync* {
        if (position.dx > 200) {
381
          yield const TestAnnotationTarget(cursor: SystemMouseCursors.forbidden);
382
        } else if (position.dx > 100) {
383
          yield const TestAnnotationTarget(cursor: SystemMouseCursors.click);
384 385 386 387 388 389
        } else {}
      },
      logCursors: logCursors,
    );

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

    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"
404
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
405 406 407 408 409 410 411 412 413
      _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"
414
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
415 416 417 418 419 420 421 422 423
      _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"
424
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440
      _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,
}) {
441
  final double devicePixelRatio = RendererBinding.instance.platformDispatcher.implicitView!.devicePixelRatio;
442 443
  return ui.PointerData(
    change: change,
444 445
    physicalX: logicalPosition.dx * devicePixelRatio,
    physicalY: logicalPosition.dy * devicePixelRatio,
446 447 448 449 450 451
    kind: kind,
    device: device,
  );
}

class _CursorUpdateDetails extends MethodCall {
452
  const _CursorUpdateDetails(super.method, Map<String, dynamic> super.arguments);
453 454 455 456

  _CursorUpdateDetails.wrap(MethodCall call)
    : super(call.method, Map<String, dynamic>.from(call.arguments as Map<dynamic, dynamic>));

457 458 459 460
  _CursorUpdateDetails.activateSystemCursor({
    required int device,
    required String kind,
  }) : this('activateSystemCursor', <String, dynamic>{'device': device, 'kind': kind});
461 462 463 464
  @override
  Map<String, dynamic> get arguments => super.arguments as Map<String, dynamic>;

  @override
465
  bool operator ==(Object other) {
466
    if (identical(other, this)) {
467
      return true;
468 469
    }
    if (other.runtimeType != runtimeType) {
470
      return false;
471
    }
472 473 474 475 476 477 478 479 480 481
    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
482
  int get hashCode => Object.hash(method, arguments);
483 484 485 486 487 488

  @override
  String toString() {
    return '_CursorUpdateDetails(method: $method, arguments: $arguments)';
  }
}