// 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'); }); }); }