| // Copyright 2018 The Chromium 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 'package:flutter/foundation.dart'; |
| import 'package:flutter/gestures.dart'; |
| import 'package:flutter/scheduler.dart'; |
| import 'package:flutter/services.dart'; |
| |
| import '../flutter_test_alternative.dart'; |
| |
| typedef HandleEventCallback = void Function(PointerEvent event); |
| |
| class TestGestureFlutterBinding extends BindingBase with ServicesBinding, SchedulerBinding, GestureBinding { |
| HandleEventCallback callback; |
| |
| @override |
| void handleEvent(PointerEvent event, HitTestEntry entry) { |
| super.handleEvent(event, entry); |
| if (callback != null) { |
| callback(event); |
| } |
| } |
| } |
| |
| TestGestureFlutterBinding _binding = TestGestureFlutterBinding(); |
| |
| void ensureTestGestureBinding() { |
| _binding ??= TestGestureFlutterBinding(); |
| assert(GestureBinding.instance != null); |
| } |
| |
| void main() { |
| setUp(ensureTestGestureBinding); |
| |
| group(MouseTracker, () { |
| final List<PointerEnterEvent> enter = <PointerEnterEvent>[]; |
| final List<PointerHoverEvent> move = <PointerHoverEvent>[]; |
| final List<PointerExitEvent> exit = <PointerExitEvent>[]; |
| final MouseTrackerAnnotation annotation = MouseTrackerAnnotation( |
| onEnter: (PointerEnterEvent event) => enter.add(event), |
| onHover: (PointerHoverEvent event) => move.add(event), |
| onExit: (PointerExitEvent event) => exit.add(event), |
| ); |
| // Only respond to some mouse events. |
| final MouseTrackerAnnotation partialAnnotation = MouseTrackerAnnotation( |
| onEnter: (PointerEnterEvent event) => enter.add(event), |
| onHover: (PointerHoverEvent event) => move.add(event), |
| ); |
| bool isInHitRegionOne; |
| bool isInHitRegionTwo; |
| MouseTracker tracker; |
| |
| void clear() { |
| enter.clear(); |
| exit.clear(); |
| move.clear(); |
| } |
| |
| setUp(() { |
| clear(); |
| isInHitRegionOne = true; |
| isInHitRegionTwo = false; |
| tracker = MouseTracker( |
| GestureBinding.instance.pointerRouter, |
| (Offset _) sync* { |
| if (isInHitRegionOne) |
| yield annotation; |
| else if (isInHitRegionTwo) |
| yield partialAnnotation; |
| }, |
| ); |
| }); |
| |
| test('receives and processes mouse hover events', () { |
| final ui.PointerDataPacket packet1 = ui.PointerDataPacket(data: <ui.PointerData>[ |
| ui.PointerData( |
| change: ui.PointerChange.hover, |
| physicalX: 0.0 * ui.window.devicePixelRatio, |
| physicalY: 0.0 * ui.window.devicePixelRatio, |
| kind: PointerDeviceKind.mouse, |
| ), |
| ]); |
| final ui.PointerDataPacket packet2 = ui.PointerDataPacket(data: <ui.PointerData>[ |
| ui.PointerData( |
| change: ui.PointerChange.hover, |
| physicalX: 1.0 * ui.window.devicePixelRatio, |
| physicalY: 101.0 * ui.window.devicePixelRatio, |
| kind: PointerDeviceKind.mouse, |
| ), |
| ]); |
| final ui.PointerDataPacket packet3 = ui.PointerDataPacket(data: <ui.PointerData>[ |
| ui.PointerData( |
| change: ui.PointerChange.remove, |
| physicalX: 1.0 * ui.window.devicePixelRatio, |
| physicalY: 201.0 * ui.window.devicePixelRatio, |
| kind: PointerDeviceKind.mouse, |
| ), |
| ]); |
| final ui.PointerDataPacket packet4 = ui.PointerDataPacket(data: <ui.PointerData>[ |
| ui.PointerData( |
| change: ui.PointerChange.hover, |
| physicalX: 1.0 * ui.window.devicePixelRatio, |
| physicalY: 301.0 * ui.window.devicePixelRatio, |
| kind: PointerDeviceKind.mouse, |
| ), |
| ]); |
| final ui.PointerDataPacket packet5 = ui.PointerDataPacket(data: <ui.PointerData>[ |
| ui.PointerData( |
| change: ui.PointerChange.hover, |
| physicalX: 1.0 * ui.window.devicePixelRatio, |
| physicalY: 401.0 * ui.window.devicePixelRatio, |
| kind: PointerDeviceKind.mouse, |
| device: 1, |
| ), |
| ]); |
| tracker.attachAnnotation(annotation); |
| isInHitRegionOne = true; |
| ui.window.onPointerDataPacket(packet1); |
| tracker.collectMousePositions(); |
| expect(enter.length, equals(1), reason: 'enter contains $enter'); |
| expect(enter.first.position, equals(const Offset(0.0, 0.0))); |
| expect(enter.first.device, equals(0)); |
| expect(enter.first.runtimeType, equals(PointerEnterEvent)); |
| expect(exit.length, equals(0), reason: 'exit contains $exit'); |
| expect(move.length, equals(1), reason: 'move contains $move'); |
| expect(move.first.position, equals(const Offset(0.0, 0.0))); |
| expect(move.first.device, equals(0)); |
| expect(move.first.runtimeType, equals(PointerHoverEvent)); |
| clear(); |
| |
| ui.window.onPointerDataPacket(packet2); |
| tracker.collectMousePositions(); |
| expect(enter.length, equals(0), reason: 'enter contains $enter'); |
| expect(exit.length, equals(0), reason: 'exit contains $exit'); |
| expect(move.length, equals(1), reason: 'move contains $move'); |
| expect(move.first.position, equals(const Offset(1.0, 101.0))); |
| expect(move.first.device, equals(0)); |
| expect(move.first.runtimeType, equals(PointerHoverEvent)); |
| clear(); |
| |
| ui.window.onPointerDataPacket(packet3); |
| tracker.collectMousePositions(); |
| expect(enter.length, equals(0), reason: 'enter contains $enter'); |
| expect(move.length, equals(0), reason: 'move contains $move'); |
| expect(exit.length, equals(1), reason: 'exit contains $exit'); |
| expect(exit.first.position, equals(const Offset(1.0, 201.0))); |
| expect(exit.first.device, equals(0)); |
| expect(exit.first.runtimeType, equals(PointerExitEvent)); |
| |
| clear(); |
| ui.window.onPointerDataPacket(packet4); |
| tracker.collectMousePositions(); |
| expect(enter.length, equals(1), reason: 'enter contains $enter'); |
| expect(enter.first.position, equals(const Offset(1.0, 301.0))); |
| expect(enter.first.device, equals(0)); |
| expect(enter.first.runtimeType, equals(PointerEnterEvent)); |
| expect(exit.length, equals(0), reason: 'exit contains $exit'); |
| expect(move.length, equals(1), reason: 'move contains $move'); |
| expect(move.first.position, equals(const Offset(1.0, 301.0))); |
| expect(move.first.device, equals(0)); |
| expect(move.first.runtimeType, equals(PointerHoverEvent)); |
| |
| // add in a second mouse simultaneously. |
| clear(); |
| ui.window.onPointerDataPacket(packet5); |
| tracker.collectMousePositions(); |
| expect(enter.length, equals(1), reason: 'enter contains $enter'); |
| expect(enter.first.position, equals(const Offset(1.0, 401.0))); |
| expect(enter.first.device, equals(1)); |
| expect(enter.first.runtimeType, equals(PointerEnterEvent)); |
| expect(exit.length, equals(0), reason: 'exit contains $exit'); |
| expect(move.length, equals(2), reason: 'move contains $move'); |
| expect(move.first.position, equals(const Offset(1.0, 301.0))); |
| expect(move.first.device, equals(0)); |
| expect(move.first.runtimeType, equals(PointerHoverEvent)); |
| expect(move.last.position, equals(const Offset(1.0, 401.0))); |
| expect(move.last.device, equals(1)); |
| expect(move.last.runtimeType, equals(PointerHoverEvent)); |
| }); |
| test('detects exit when annotated layer no longer hit', () { |
| final ui.PointerDataPacket packet1 = ui.PointerDataPacket(data: <ui.PointerData>[ |
| ui.PointerData( |
| change: ui.PointerChange.hover, |
| physicalX: 0.0 * ui.window.devicePixelRatio, |
| physicalY: 0.0 * ui.window.devicePixelRatio, |
| kind: PointerDeviceKind.mouse, |
| ), |
| ui.PointerData( |
| change: ui.PointerChange.hover, |
| physicalX: 1.0 * ui.window.devicePixelRatio, |
| physicalY: 101.0 * ui.window.devicePixelRatio, |
| kind: PointerDeviceKind.mouse, |
| ), |
| ]); |
| final ui.PointerDataPacket packet2 = ui.PointerDataPacket(data: <ui.PointerData>[ |
| ui.PointerData( |
| change: ui.PointerChange.hover, |
| physicalX: 1.0 * ui.window.devicePixelRatio, |
| physicalY: 201.0 * ui.window.devicePixelRatio, |
| kind: PointerDeviceKind.mouse, |
| ), |
| ]); |
| isInHitRegionOne = true; |
| tracker.attachAnnotation(annotation); |
| |
| ui.window.onPointerDataPacket(packet1); |
| tracker.collectMousePositions(); |
| expect(enter.length, equals(1), reason: 'enter contains $enter'); |
| expect(enter.first.position, equals(const Offset(1.0, 101.0))); |
| expect(enter.first.device, equals(0)); |
| expect(enter.first.runtimeType, equals(PointerEnterEvent)); |
| expect(move.length, equals(1), reason: 'move contains $move'); |
| expect(move.first.position, equals(const Offset(1.0, 101.0))); |
| expect(move.first.device, equals(0)); |
| expect(move.first.runtimeType, equals(PointerHoverEvent)); |
| expect(exit.length, equals(0), reason: 'exit contains $exit'); |
| // Simulate layer going away by detaching it. |
| clear(); |
| isInHitRegionOne = false; |
| |
| ui.window.onPointerDataPacket(packet2); |
| tracker.collectMousePositions(); |
| expect(enter.length, equals(0), reason: 'enter contains $enter'); |
| expect(move.length, equals(0), reason: 'enter contains $move'); |
| expect(exit.length, equals(1), reason: 'enter contains $exit'); |
| expect(exit.first.position, const Offset(1.0, 201.0)); |
| expect(exit.first.device, equals(0)); |
| expect(exit.first.runtimeType, equals(PointerExitEvent)); |
| |
| // Actually detach annotation. Shouldn't receive hit. |
| tracker.detachAnnotation(annotation); |
| clear(); |
| isInHitRegionOne = false; |
| |
| ui.window.onPointerDataPacket(packet2); |
| tracker.collectMousePositions(); |
| expect(enter.length, equals(0), reason: 'enter contains $enter'); |
| expect(move.length, equals(0), reason: 'enter contains $move'); |
| expect(exit.length, equals(0), reason: 'enter contains $exit'); |
| }); |
| |
| test("don't flip out if not all mouse events are listened to", () { |
| final ui.PointerDataPacket packet = ui.PointerDataPacket(data: <ui.PointerData>[ |
| ui.PointerData( |
| change: ui.PointerChange.hover, |
| physicalX: 1.0 * ui.window.devicePixelRatio, |
| physicalY: 101.0 * ui.window.devicePixelRatio, |
| kind: PointerDeviceKind.mouse, |
| ), |
| ]); |
| |
| isInHitRegionOne = false; |
| isInHitRegionTwo = true; |
| tracker.attachAnnotation(partialAnnotation); |
| |
| ui.window.onPointerDataPacket(packet); |
| tracker.collectMousePositions(); |
| tracker.detachAnnotation(partialAnnotation); |
| isInHitRegionTwo = false; |
| }); |
| test('detects exit when mouse goes away', () { |
| final ui.PointerDataPacket packet1 = ui.PointerDataPacket(data: <ui.PointerData>[ |
| ui.PointerData( |
| change: ui.PointerChange.hover, |
| physicalX: 0.0 * ui.window.devicePixelRatio, |
| physicalY: 0.0 * ui.window.devicePixelRatio, |
| kind: PointerDeviceKind.mouse, |
| ), |
| ui.PointerData( |
| change: ui.PointerChange.hover, |
| physicalX: 1.0 * ui.window.devicePixelRatio, |
| physicalY: 101.0 * ui.window.devicePixelRatio, |
| kind: PointerDeviceKind.mouse, |
| ), |
| ]); |
| final ui.PointerDataPacket packet2 = ui.PointerDataPacket(data: <ui.PointerData>[ |
| ui.PointerData( |
| change: ui.PointerChange.remove, |
| physicalX: 1.0 * ui.window.devicePixelRatio, |
| physicalY: 201.0 * ui.window.devicePixelRatio, |
| kind: PointerDeviceKind.mouse, |
| ), |
| ]); |
| isInHitRegionOne = true; |
| tracker.attachAnnotation(annotation); |
| ui.window.onPointerDataPacket(packet1); |
| tracker.collectMousePositions(); |
| ui.window.onPointerDataPacket(packet2); |
| tracker.collectMousePositions(); |
| expect(enter.length, equals(1), reason: 'enter contains $enter'); |
| expect(enter.first.position, equals(const Offset(1.0, 101.0))); |
| expect(enter.first.delta, equals(const Offset(1.0, 101.0))); |
| expect(enter.first.device, equals(0)); |
| expect(enter.first.runtimeType, equals(PointerEnterEvent)); |
| expect(move.length, equals(1), reason: 'move contains $move'); |
| expect(move.first.position, equals(const Offset(1.0, 101.0))); |
| expect(move.first.delta, equals(const Offset(1.0, 101.0))); |
| expect(move.first.device, equals(0)); |
| expect(move.first.runtimeType, equals(PointerHoverEvent)); |
| expect(exit.length, equals(1), reason: 'exit contains $exit'); |
| expect(exit.first.position, equals(const Offset(1.0, 201.0))); |
| expect(exit.first.delta, equals(const Offset(0.0, 0.0))); |
| expect(exit.first.device, equals(0)); |
| expect(exit.first.runtimeType, equals(PointerExitEvent)); |
| }); |
| test('handles mouse down and move', () { |
| final ui.PointerDataPacket packet1 = ui.PointerDataPacket(data: <ui.PointerData>[ |
| ui.PointerData( |
| change: ui.PointerChange.hover, |
| physicalX: 0.0 * ui.window.devicePixelRatio, |
| physicalY: 0.0 * ui.window.devicePixelRatio, |
| kind: PointerDeviceKind.mouse, |
| ), |
| ui.PointerData( |
| change: ui.PointerChange.hover, |
| physicalX: 1.0 * ui.window.devicePixelRatio, |
| physicalY: 101.0 * ui.window.devicePixelRatio, |
| kind: PointerDeviceKind.mouse, |
| ), |
| ]); |
| final ui.PointerDataPacket packet2 = ui.PointerDataPacket(data: <ui.PointerData>[ |
| ui.PointerData( |
| change: ui.PointerChange.down, |
| physicalX: 1.0 * ui.window.devicePixelRatio, |
| physicalY: 101.0 * ui.window.devicePixelRatio, |
| kind: PointerDeviceKind.mouse, |
| ), |
| ui.PointerData( |
| change: ui.PointerChange.move, |
| physicalX: 1.0 * ui.window.devicePixelRatio, |
| physicalY: 201.0 * ui.window.devicePixelRatio, |
| kind: PointerDeviceKind.mouse, |
| ), |
| ]); |
| isInHitRegionOne = true; |
| tracker.attachAnnotation(annotation); |
| ui.window.onPointerDataPacket(packet1); |
| tracker.collectMousePositions(); |
| ui.window.onPointerDataPacket(packet2); |
| tracker.collectMousePositions(); |
| expect(enter.length, equals(1), reason: 'enter contains $enter'); |
| expect(enter.first.position, equals(const Offset(1.0, 101.0))); |
| expect(enter.first.device, equals(0)); |
| expect(enter.first.runtimeType, equals(PointerEnterEvent)); |
| expect(move.length, equals(1), reason: 'move contains $move'); |
| expect(move.first.position, equals(const Offset(1.0, 101.0))); |
| expect(move.first.device, equals(0)); |
| expect(move.first.runtimeType, equals(PointerHoverEvent)); |
| expect(exit.length, equals(0), reason: 'exit contains $exit'); |
| }); |
| }); |
| } |