| // 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 'package:flutter/foundation.dart'; |
| import 'package:flutter/gestures.dart'; |
| import 'package:flutter/material.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/services.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| class HoverClient extends StatefulWidget { |
| const HoverClient({ |
| Key? key, |
| this.onHover, |
| this.child, |
| this.onEnter, |
| this.onExit, |
| }) : super(key: key); |
| |
| final ValueChanged<bool>? onHover; |
| final Widget? child; |
| final VoidCallback? onEnter; |
| final VoidCallback? onExit; |
| |
| @override |
| HoverClientState createState() => HoverClientState(); |
| } |
| |
| class HoverClientState extends State<HoverClient> { |
| void _onExit(PointerExitEvent details) { |
| widget.onExit?.call(); |
| widget.onHover?.call(false); |
| } |
| |
| void _onEnter(PointerEnterEvent details) { |
| widget.onEnter?.call(); |
| widget.onHover?.call(true); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| return MouseRegion( |
| onEnter: _onEnter, |
| onExit: _onExit, |
| child: widget.child, |
| ); |
| } |
| } |
| |
| class HoverFeedback extends StatefulWidget { |
| const HoverFeedback({Key? key, this.onEnter, this.onExit}) : super(key: key); |
| |
| final VoidCallback? onEnter; |
| final VoidCallback? onExit; |
| |
| @override |
| State<HoverFeedback> createState() => _HoverFeedbackState(); |
| } |
| |
| class _HoverFeedbackState extends State<HoverFeedback> { |
| bool _hovering = false; |
| |
| @override |
| Widget build(BuildContext context) { |
| return Directionality( |
| textDirection: TextDirection.ltr, |
| child: HoverClient( |
| onHover: (bool hovering) => setState(() => _hovering = hovering), |
| onEnter: widget.onEnter, |
| onExit: widget.onExit, |
| child: Text(_hovering ? 'HOVERING' : 'not hovering'), |
| ), |
| ); |
| } |
| } |
| |
| void main() { |
| testWidgets('onEnter and onExit can be triggered with mouse buttons pressed', (WidgetTester tester) async { |
| PointerEnterEvent? enter; |
| PointerExitEvent? exit; |
| await tester.pumpWidget(Center( |
| child: MouseRegion( |
| child: Container( |
| color: const Color.fromARGB(0xff, 0xff, 0x00, 0x00), |
| width: 100.0, |
| height: 100.0, |
| ), |
| onEnter: (PointerEnterEvent details) => enter = details, |
| onExit: (PointerExitEvent details) => exit = details, |
| ), |
| )); |
| |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| await gesture.addPointer(location: Offset.zero); |
| await gesture.down(Offset.zero); // Press the mouse button. |
| addTearDown(gesture.removePointer); |
| await tester.pump(); |
| enter = null; |
| exit = null; |
| // Trigger the enter event. |
| await gesture.moveTo(const Offset(400.0, 300.0)); |
| expect(enter, isNotNull); |
| expect(enter!.position, equals(const Offset(400.0, 300.0))); |
| expect(enter!.localPosition, equals(const Offset(50.0, 50.0))); |
| expect(exit, isNull); |
| |
| // Trigger the exit event. |
| await gesture.moveTo(const Offset(1.0, 1.0)); |
| expect(exit, isNotNull); |
| expect(exit!.position, equals(const Offset(1.0, 1.0))); |
| expect(exit!.localPosition, equals(const Offset(-349.0, -249.0))); |
| }); |
| |
| testWidgets('detects pointer enter', (WidgetTester tester) async { |
| PointerEnterEvent? enter; |
| PointerHoverEvent? move; |
| PointerExitEvent? exit; |
| await tester.pumpWidget(Center( |
| child: MouseRegion( |
| child: Container( |
| color: const Color.fromARGB(0xff, 0xff, 0x00, 0x00), |
| width: 100.0, |
| height: 100.0, |
| ), |
| onEnter: (PointerEnterEvent details) => enter = details, |
| onHover: (PointerHoverEvent details) => move = details, |
| onExit: (PointerExitEvent details) => exit = details, |
| ), |
| )); |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| await gesture.addPointer(location: Offset.zero); |
| addTearDown(gesture.removePointer); |
| await tester.pump(); |
| move = null; |
| enter = null; |
| exit = null; |
| await gesture.moveTo(const Offset(400.0, 300.0)); |
| expect(move, isNotNull); |
| expect(move!.position, equals(const Offset(400.0, 300.0))); |
| expect(move!.localPosition, equals(const Offset(50.0, 50.0))); |
| expect(enter, isNotNull); |
| expect(enter!.position, equals(const Offset(400.0, 300.0))); |
| expect(enter!.localPosition, equals(const Offset(50.0, 50.0))); |
| expect(exit, isNull); |
| }); |
| |
| testWidgets('detects pointer exiting', (WidgetTester tester) async { |
| PointerEnterEvent? enter; |
| PointerHoverEvent? move; |
| PointerExitEvent? exit; |
| await tester.pumpWidget(Center( |
| child: MouseRegion( |
| child: Container( |
| color: const Color.fromARGB(0xff, 0xff, 0x00, 0x00), |
| width: 100.0, |
| height: 100.0, |
| ), |
| onEnter: (PointerEnterEvent details) => enter = details, |
| onHover: (PointerHoverEvent details) => move = details, |
| onExit: (PointerExitEvent details) => exit = details, |
| ), |
| )); |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| await gesture.addPointer(location: Offset.zero); |
| addTearDown(gesture.removePointer); |
| await gesture.moveTo(const Offset(400.0, 300.0)); |
| await tester.pump(); |
| move = null; |
| enter = null; |
| exit = null; |
| await gesture.moveTo(const Offset(1.0, 1.0)); |
| expect(move, isNull); |
| expect(enter, isNull); |
| expect(exit, isNotNull); |
| expect(exit!.position, equals(const Offset(1.0, 1.0))); |
| expect(exit!.localPosition, equals(const Offset(-349.0, -249.0))); |
| }); |
| |
| testWidgets('triggers pointer enter when a mouse is connected', (WidgetTester tester) async { |
| PointerEnterEvent? enter; |
| PointerHoverEvent? move; |
| PointerExitEvent? exit; |
| await tester.pumpWidget(Center( |
| child: MouseRegion( |
| child: const SizedBox( |
| width: 100.0, |
| height: 100.0, |
| ), |
| onEnter: (PointerEnterEvent details) => enter = details, |
| onHover: (PointerHoverEvent details) => move = details, |
| onExit: (PointerExitEvent details) => exit = details, |
| ), |
| )); |
| await tester.pump(); |
| |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| await gesture.addPointer(location: const Offset(400, 300)); |
| addTearDown(gesture.removePointer); |
| expect(move, isNull); |
| expect(enter, isNotNull); |
| expect(enter!.position, equals(const Offset(400.0, 300.0))); |
| expect(enter!.localPosition, equals(const Offset(50.0, 50.0))); |
| expect(exit, isNull); |
| }); |
| |
| testWidgets('triggers pointer exit when a mouse is disconnected', (WidgetTester tester) async { |
| PointerEnterEvent? enter; |
| PointerHoverEvent? move; |
| PointerExitEvent? exit; |
| await tester.pumpWidget(Center( |
| child: MouseRegion( |
| child: const SizedBox( |
| width: 100.0, |
| height: 100.0, |
| ), |
| onEnter: (PointerEnterEvent details) => enter = details, |
| onHover: (PointerHoverEvent details) => move = details, |
| onExit: (PointerExitEvent details) => exit = details, |
| ), |
| )); |
| await tester.pump(); |
| |
| TestGesture? gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| await gesture.addPointer(location: const Offset(400, 300)); |
| addTearDown(() => gesture?.removePointer); |
| await tester.pump(); |
| move = null; |
| enter = null; |
| exit = null; |
| await gesture.removePointer(); |
| gesture = null; |
| expect(move, isNull); |
| expect(enter, isNull); |
| expect(exit, isNotNull); |
| expect(exit!.position, equals(const Offset(400.0, 300.0))); |
| expect(exit!.localPosition, equals(const Offset(50.0, 50.0))); |
| exit = null; |
| await tester.pump(); |
| expect(move, isNull); |
| expect(enter, isNull); |
| expect(exit, isNull); |
| }); |
| |
| testWidgets('triggers pointer enter when widget appears', (WidgetTester tester) async { |
| PointerEnterEvent? enter; |
| PointerHoverEvent? move; |
| PointerExitEvent? exit; |
| await tester.pumpWidget(const Center( |
| child: SizedBox( |
| width: 100.0, |
| height: 100.0, |
| ), |
| )); |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| await gesture.addPointer(location: Offset.zero); |
| addTearDown(gesture.removePointer); |
| await gesture.moveTo(const Offset(400.0, 300.0)); |
| await tester.pump(); |
| expect(enter, isNull); |
| expect(move, isNull); |
| expect(exit, isNull); |
| await tester.pumpWidget(Center( |
| child: MouseRegion( |
| child: const SizedBox( |
| width: 100.0, |
| height: 100.0, |
| ), |
| onEnter: (PointerEnterEvent details) => enter = details, |
| onHover: (PointerHoverEvent details) => move = details, |
| onExit: (PointerExitEvent details) => exit = details, |
| ), |
| )); |
| await tester.pump(); |
| expect(move, isNull); |
| expect(enter, isNotNull); |
| expect(enter!.position, equals(const Offset(400.0, 300.0))); |
| expect(enter!.localPosition, equals(const Offset(50.0, 50.0))); |
| expect(exit, isNull); |
| }); |
| |
| testWidgets("doesn't trigger pointer exit when widget disappears", (WidgetTester tester) async { |
| PointerEnterEvent? enter; |
| PointerHoverEvent? move; |
| PointerExitEvent? exit; |
| await tester.pumpWidget(Center( |
| child: MouseRegion( |
| child: const SizedBox( |
| width: 100.0, |
| height: 100.0, |
| ), |
| onEnter: (PointerEnterEvent details) => enter = details, |
| onHover: (PointerHoverEvent details) => move = details, |
| onExit: (PointerExitEvent details) => exit = details, |
| ), |
| )); |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| await gesture.addPointer(location: Offset.zero); |
| addTearDown(gesture.removePointer); |
| await gesture.moveTo(const Offset(400.0, 300.0)); |
| await tester.pump(); |
| move = null; |
| enter = null; |
| exit = null; |
| await tester.pumpWidget(const Center( |
| child: SizedBox( |
| width: 100.0, |
| height: 100.0, |
| ), |
| )); |
| expect(enter, isNull); |
| expect(move, isNull); |
| expect(exit, isNull); |
| }); |
| |
| testWidgets('triggers pointer enter when widget moves in', (WidgetTester tester) async { |
| PointerEnterEvent? enter; |
| PointerHoverEvent? move; |
| PointerExitEvent? exit; |
| await tester.pumpWidget(Container( |
| alignment: Alignment.topLeft, |
| child: MouseRegion( |
| child: const SizedBox( |
| width: 100.0, |
| height: 100.0, |
| ), |
| onEnter: (PointerEnterEvent details) => enter = details, |
| onHover: (PointerHoverEvent details) => move = details, |
| onExit: (PointerExitEvent details) => exit = details, |
| ), |
| )); |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| await gesture.addPointer(location: const Offset(401.0, 301.0)); |
| addTearDown(gesture.removePointer); |
| await tester.pump(); |
| expect(enter, isNull); |
| expect(move, isNull); |
| expect(exit, isNull); |
| await tester.pumpWidget(Container( |
| alignment: Alignment.center, |
| child: MouseRegion( |
| child: const SizedBox( |
| width: 100.0, |
| height: 100.0, |
| ), |
| onEnter: (PointerEnterEvent details) => enter = details, |
| onHover: (PointerHoverEvent details) => move = details, |
| onExit: (PointerExitEvent details) => exit = details, |
| ), |
| )); |
| await tester.pump(); |
| expect(enter, isNotNull); |
| expect(enter!.position, equals(const Offset(401.0, 301.0))); |
| expect(enter!.localPosition, equals(const Offset(51.0, 51.0))); |
| expect(move, isNull); |
| expect(exit, isNull); |
| }); |
| |
| testWidgets('triggers pointer exit when widget moves out', (WidgetTester tester) async { |
| PointerEnterEvent? enter; |
| PointerHoverEvent? move; |
| PointerExitEvent? exit; |
| await tester.pumpWidget(Container( |
| alignment: Alignment.center, |
| child: MouseRegion( |
| child: const SizedBox( |
| width: 100.0, |
| height: 100.0, |
| ), |
| onEnter: (PointerEnterEvent details) => enter = details, |
| onHover: (PointerHoverEvent details) => move = details, |
| onExit: (PointerExitEvent details) => exit = details, |
| ), |
| )); |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| await gesture.addPointer(location: const Offset(400, 300)); |
| addTearDown(gesture.removePointer); |
| await tester.pump(); |
| enter = null; |
| move = null; |
| exit = null; |
| await tester.pumpWidget(Container( |
| alignment: Alignment.topLeft, |
| child: MouseRegion( |
| child: const SizedBox( |
| width: 100.0, |
| height: 100.0, |
| ), |
| onEnter: (PointerEnterEvent details) => enter = details, |
| onHover: (PointerHoverEvent details) => move = details, |
| onExit: (PointerExitEvent details) => exit = details, |
| ), |
| )); |
| await tester.pump(); |
| expect(enter, isNull); |
| expect(move, isNull); |
| expect(exit, isNotNull); |
| expect(exit!.position, equals(const Offset(400, 300))); |
| expect(exit!.localPosition, equals(const Offset(50, 50))); |
| }); |
| |
| testWidgets('detects hover from touch devices', (WidgetTester tester) async { |
| PointerEnterEvent? enter; |
| PointerHoverEvent? move; |
| PointerExitEvent? exit; |
| await tester.pumpWidget(Center( |
| child: MouseRegion( |
| child: Container( |
| color: const Color.fromARGB(0xff, 0xff, 0x00, 0x00), |
| width: 100.0, |
| height: 100.0, |
| ), |
| onEnter: (PointerEnterEvent details) => enter = details, |
| onHover: (PointerHoverEvent details) => move = details, |
| onExit: (PointerExitEvent details) => exit = details, |
| ), |
| )); |
| final TestGesture gesture = await tester.createGesture(); |
| await gesture.addPointer(location: Offset.zero); |
| addTearDown(gesture.removePointer); |
| await tester.pump(); |
| move = null; |
| enter = null; |
| exit = null; |
| await gesture.moveTo(const Offset(400.0, 300.0)); |
| expect(move, isNotNull); |
| expect(move!.position, equals(const Offset(400.0, 300.0))); |
| expect(move!.localPosition, equals(const Offset(50.0, 50.0))); |
| expect(enter, isNull); |
| expect(exit, isNull); |
| }); |
| |
| testWidgets('Hover works with nested listeners', (WidgetTester tester) async { |
| final UniqueKey key1 = UniqueKey(); |
| final UniqueKey key2 = UniqueKey(); |
| final List<PointerEnterEvent> enter1 = <PointerEnterEvent>[]; |
| final List<PointerHoverEvent> move1 = <PointerHoverEvent>[]; |
| final List<PointerExitEvent> exit1 = <PointerExitEvent>[]; |
| final List<PointerEnterEvent> enter2 = <PointerEnterEvent>[]; |
| final List<PointerHoverEvent> move2 = <PointerHoverEvent>[]; |
| final List<PointerExitEvent> exit2 = <PointerExitEvent>[]; |
| void clearLists() { |
| enter1.clear(); |
| move1.clear(); |
| exit1.clear(); |
| enter2.clear(); |
| move2.clear(); |
| exit2.clear(); |
| } |
| |
| await tester.pumpWidget(Container()); |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| addTearDown(gesture.removePointer); |
| await gesture.moveTo(const Offset(400.0, 0.0)); |
| await tester.pump(); |
| await tester.pumpWidget( |
| Column( |
| mainAxisAlignment: MainAxisAlignment.center, |
| children: <Widget>[ |
| MouseRegion( |
| onEnter: (PointerEnterEvent details) => enter1.add(details), |
| onHover: (PointerHoverEvent details) => move1.add(details), |
| onExit: (PointerExitEvent details) => exit1.add(details), |
| key: key1, |
| child: Container( |
| width: 200, |
| height: 200, |
| padding: const EdgeInsets.all(50.0), |
| child: MouseRegion( |
| key: key2, |
| onEnter: (PointerEnterEvent details) => enter2.add(details), |
| onHover: (PointerHoverEvent details) => move2.add(details), |
| onExit: (PointerExitEvent details) => exit2.add(details), |
| child: Container(), |
| ), |
| ), |
| ), |
| ], |
| ), |
| ); |
| Offset center = tester.getCenter(find.byKey(key2)); |
| await gesture.moveTo(center); |
| await tester.pump(); |
| expect(move2, isNotEmpty); |
| expect(enter2, isNotEmpty); |
| expect(exit2, isEmpty); |
| expect(move1, isNotEmpty); |
| expect(move1.last.position, equals(center)); |
| expect(enter1, isNotEmpty); |
| expect(enter1.last.position, equals(center)); |
| expect(exit1, isEmpty); |
| clearLists(); |
| |
| // Now make sure that exiting the child only triggers the child exit, not |
| // the parent too. |
| center = center - const Offset(75.0, 0.0); |
| await gesture.moveTo(center); |
| await tester.pumpAndSettle(); |
| expect(move2, isEmpty); |
| expect(enter2, isEmpty); |
| expect(exit2, isNotEmpty); |
| expect(move1, isNotEmpty); |
| expect(move1.last.position, equals(center)); |
| expect(enter1, isEmpty); |
| expect(exit1, isEmpty); |
| clearLists(); |
| }); |
| |
| testWidgets('Hover transfers between two listeners', (WidgetTester tester) async { |
| final UniqueKey key1 = UniqueKey(); |
| final UniqueKey key2 = UniqueKey(); |
| final List<PointerEnterEvent> enter1 = <PointerEnterEvent>[]; |
| final List<PointerHoverEvent> move1 = <PointerHoverEvent>[]; |
| final List<PointerExitEvent> exit1 = <PointerExitEvent>[]; |
| final List<PointerEnterEvent> enter2 = <PointerEnterEvent>[]; |
| final List<PointerHoverEvent> move2 = <PointerHoverEvent>[]; |
| final List<PointerExitEvent> exit2 = <PointerExitEvent>[]; |
| void clearLists() { |
| enter1.clear(); |
| move1.clear(); |
| exit1.clear(); |
| enter2.clear(); |
| move2.clear(); |
| exit2.clear(); |
| } |
| |
| await tester.pumpWidget(Container()); |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| addTearDown(gesture.removePointer); |
| await gesture.moveTo(const Offset(400.0, 0.0)); |
| await tester.pump(); |
| await tester.pumpWidget( |
| Column( |
| mainAxisAlignment: MainAxisAlignment.center, |
| children: <Widget>[ |
| MouseRegion( |
| key: key1, |
| child: const SizedBox( |
| width: 100.0, |
| height: 100.0, |
| ), |
| onEnter: (PointerEnterEvent details) => enter1.add(details), |
| onHover: (PointerHoverEvent details) => move1.add(details), |
| onExit: (PointerExitEvent details) => exit1.add(details), |
| ), |
| MouseRegion( |
| key: key2, |
| child: const SizedBox( |
| width: 100.0, |
| height: 100.0, |
| ), |
| onEnter: (PointerEnterEvent details) => enter2.add(details), |
| onHover: (PointerHoverEvent details) => move2.add(details), |
| onExit: (PointerExitEvent details) => exit2.add(details), |
| ), |
| ], |
| ), |
| ); |
| final Offset center1 = tester.getCenter(find.byKey(key1)); |
| final Offset center2 = tester.getCenter(find.byKey(key2)); |
| await gesture.moveTo(center1); |
| await tester.pump(); |
| expect(move1, isNotEmpty); |
| expect(move1.last.position, equals(center1)); |
| expect(enter1, isNotEmpty); |
| expect(enter1.last.position, equals(center1)); |
| expect(exit1, isEmpty); |
| expect(move2, isEmpty); |
| expect(enter2, isEmpty); |
| expect(exit2, isEmpty); |
| clearLists(); |
| await gesture.moveTo(center2); |
| await tester.pump(); |
| expect(move1, isEmpty); |
| expect(enter1, isEmpty); |
| expect(exit1, isNotEmpty); |
| expect(exit1.last.position, equals(center2)); |
| expect(move2, isNotEmpty); |
| expect(move2.last.position, equals(center2)); |
| expect(enter2, isNotEmpty); |
| expect(enter2.last.position, equals(center2)); |
| expect(exit2, isEmpty); |
| clearLists(); |
| await gesture.moveTo(const Offset(400.0, 450.0)); |
| await tester.pump(); |
| expect(move1, isEmpty); |
| expect(enter1, isEmpty); |
| expect(exit1, isEmpty); |
| expect(move2, isEmpty); |
| expect(enter2, isEmpty); |
| expect(exit2, isNotEmpty); |
| expect(exit2.last.position, equals(const Offset(400.0, 450.0))); |
| clearLists(); |
| await tester.pumpWidget(Container()); |
| expect(move1, isEmpty); |
| expect(enter1, isEmpty); |
| expect(exit1, isEmpty); |
| expect(move2, isEmpty); |
| expect(enter2, isEmpty); |
| expect(exit2, isEmpty); |
| }); |
| |
| testWidgets('applies mouse cursor', (WidgetTester tester) async { |
| await tester.pumpWidget(const _Scaffold( |
| topLeft: MouseRegion( |
| cursor: SystemMouseCursors.text, |
| child: SizedBox(width: 10, height: 10), |
| ), |
| )); |
| |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| await gesture.addPointer(location: const Offset(100, 100)); |
| addTearDown(gesture.removePointer); |
| |
| await tester.pump(); |
| expect(RendererBinding.instance!.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic); |
| |
| await gesture.moveTo(const Offset(5, 5)); |
| expect(RendererBinding.instance!.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text); |
| |
| await gesture.moveTo(const Offset(100, 100)); |
| expect(RendererBinding.instance!.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic); |
| }); |
| |
| testWidgets('MouseRegion uses updated callbacks', (WidgetTester tester) async { |
| final List<String> logs = <String>[]; |
| Widget hoverableContainer({ |
| PointerEnterEventListener? onEnter, |
| PointerHoverEventListener? onHover, |
| PointerExitEventListener? onExit, |
| }) { |
| return Container( |
| alignment: Alignment.topLeft, |
| child: MouseRegion( |
| onEnter: onEnter, |
| onHover: onHover, |
| onExit: onExit, |
| child: Container( |
| color: const Color.fromARGB(0xff, 0xff, 0x00, 0x00), |
| width: 100.0, |
| height: 100.0, |
| ), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(hoverableContainer( |
| onEnter: (PointerEnterEvent details) { |
| logs.add('enter1'); |
| }, |
| onHover: (PointerHoverEvent details) { |
| logs.add('hover1'); |
| }, |
| onExit: (PointerExitEvent details) { logs.add('exit1'); }, |
| )); |
| |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| await gesture.addPointer(location: const Offset(150.0, 150.0)); |
| addTearDown(gesture.removePointer); |
| |
| // Start outside, move inside, then move outside |
| await gesture.moveTo(const Offset(150.0, 150.0)); |
| await tester.pump(); |
| expect(logs, isEmpty); |
| logs.clear(); |
| await gesture.moveTo(const Offset(50.0, 50.0)); |
| await tester.pump(); |
| await gesture.moveTo(const Offset(150.0, 150.0)); |
| await tester.pump(); |
| expect(logs, <String>['enter1', 'hover1', 'exit1']); |
| logs.clear(); |
| |
| // Same tests but with updated callbacks |
| await tester.pumpWidget(hoverableContainer( |
| onEnter: (PointerEnterEvent details) => logs.add('enter2'), |
| onHover: (PointerHoverEvent details) => logs.add('hover2'), |
| onExit: (PointerExitEvent details) => logs.add('exit2'), |
| )); |
| await gesture.moveTo(const Offset(150.0, 150.0)); |
| await tester.pump(); |
| await gesture.moveTo(const Offset(50.0, 50.0)); |
| await tester.pump(); |
| await gesture.moveTo(const Offset(150.0, 150.0)); |
| await tester.pump(); |
| expect(logs, <String>['enter2', 'hover2', 'exit2']); |
| }); |
| |
| testWidgets('needsCompositing set when parent class needsCompositing is set', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MouseRegion( |
| onEnter: (PointerEnterEvent _) {}, |
| child: const Opacity(opacity: 0.5, child: Placeholder()), |
| ), |
| ); |
| |
| RenderMouseRegion listener = tester.renderObject(find.byType(MouseRegion).first); |
| expect(listener.needsCompositing, isTrue); |
| |
| await tester.pumpWidget( |
| MouseRegion( |
| onEnter: (PointerEnterEvent _) {}, |
| child: const Placeholder(), |
| ), |
| ); |
| |
| listener = tester.renderObject(find.byType(MouseRegion).first); |
| expect(listener.needsCompositing, isFalse); |
| }); |
| |
| testWidgets('works with transform', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/31986. |
| final Key key = UniqueKey(); |
| const double scaleFactor = 2.0; |
| const double localWidth = 150.0; |
| const double localHeight = 100.0; |
| final List<PointerEvent> events = <PointerEvent>[]; |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Center( |
| child: Transform.scale( |
| scale: scaleFactor, |
| child: MouseRegion( |
| onEnter: (PointerEnterEvent event) { |
| events.add(event); |
| }, |
| onHover: (PointerHoverEvent event) { |
| events.add(event); |
| }, |
| onExit: (PointerExitEvent event) { |
| events.add(event); |
| }, |
| child: Container( |
| key: key, |
| color: Colors.blue, |
| height: localHeight, |
| width: localWidth, |
| child: const Text('Hi'), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final Offset topLeft = tester.getTopLeft(find.byKey(key)); |
| final Offset topRight = tester.getTopRight(find.byKey(key)); |
| final Offset bottomLeft = tester.getBottomLeft(find.byKey(key)); |
| expect(topRight.dx - topLeft.dx, scaleFactor * localWidth); |
| expect(bottomLeft.dy - topLeft.dy, scaleFactor * localHeight); |
| |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| await gesture.addPointer(); |
| addTearDown(gesture.removePointer); |
| await gesture.moveTo(topLeft - const Offset(1, 1)); |
| await tester.pump(); |
| expect(events, isEmpty); |
| |
| await gesture.moveTo(topLeft + const Offset(1, 1)); |
| await tester.pump(); |
| expect(events, hasLength(2)); |
| expect(events.first, isA<PointerEnterEvent>()); |
| expect(events.last, isA<PointerHoverEvent>()); |
| events.clear(); |
| |
| await gesture.moveTo(bottomLeft + const Offset(1, -1)); |
| await tester.pump(); |
| expect(events.single, isA<PointerHoverEvent>()); |
| expect(events.single.delta, const Offset(0.0, scaleFactor * localHeight - 2)); |
| events.clear(); |
| |
| await gesture.moveTo(bottomLeft + const Offset(1, 1)); |
| await tester.pump(); |
| expect(events.single, isA<PointerExitEvent>()); |
| events.clear(); |
| }); |
| |
| testWidgets('needsCompositing is always false', (WidgetTester tester) async { |
| // Pretend that we have a mouse connected. |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| await gesture.addPointer(); |
| addTearDown(gesture.removePointer); |
| |
| await tester.pumpWidget( |
| Transform.scale( |
| scale: 2.0, |
| child: const MouseRegion(opaque: false), |
| ), |
| ); |
| final RenderMouseRegion mouseRegion = tester.renderObject(find.byType(MouseRegion)); |
| expect(mouseRegion.needsCompositing, isFalse); |
| // No TransformLayer for `Transform.scale` is added because composting is |
| // not required and therefore the transform is executed on the canvas |
| // directly. (One TransformLayer is always present for the root |
| // transform.) |
| expect(tester.layers.whereType<TransformLayer>(), hasLength(1)); |
| |
| // Test that needsCompositing stays false with callback change |
| await tester.pumpWidget( |
| Transform.scale( |
| scale: 2.0, |
| child: MouseRegion( |
| opaque: false, |
| onHover: (PointerHoverEvent _) {}, |
| ), |
| ), |
| ); |
| expect(mouseRegion.needsCompositing, isFalse); |
| // If compositing was required, a dedicated TransformLayer for |
| // `Transform.scale` would be added. |
| expect(tester.layers.whereType<TransformLayer>(), hasLength(1)); |
| }); |
| |
| testWidgets("Callbacks aren't called during build", (WidgetTester tester) async { |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| addTearDown(gesture.removePointer); |
| await gesture.addPointer(location: Offset.zero); |
| |
| int numEntrances = 0; |
| int numExits = 0; |
| |
| await tester.pumpWidget( |
| Center( |
| child: HoverFeedback( |
| onEnter: () { numEntrances += 1; }, |
| onExit: () { numExits += 1; }, |
| ), |
| ), |
| ); |
| |
| await gesture.moveTo(tester.getCenter(find.byType(Text))); |
| await tester.pumpAndSettle(); |
| expect(numEntrances, equals(1)); |
| expect(numExits, equals(0)); |
| expect(find.text('HOVERING'), findsOneWidget); |
| |
| await tester.pumpWidget( |
| Container(), |
| ); |
| await tester.pump(); |
| expect(numEntrances, equals(1)); |
| expect(numExits, equals(0)); |
| |
| await tester.pumpWidget( |
| Center( |
| child: HoverFeedback( |
| onEnter: () { numEntrances += 1; }, |
| onExit: () { numExits += 1; }, |
| ), |
| ), |
| ); |
| await tester.pump(); |
| expect(numEntrances, equals(2)); |
| expect(numExits, equals(0)); |
| }); |
| |
| testWidgets("MouseRegion activate/deactivate don't duplicate annotations", (WidgetTester tester) async { |
| final GlobalKey feedbackKey = GlobalKey(); |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| await gesture.addPointer(); |
| addTearDown(gesture.removePointer); |
| |
| int numEntrances = 0; |
| int numExits = 0; |
| |
| await tester.pumpWidget( |
| Center( |
| child: HoverFeedback( |
| key: feedbackKey, |
| onEnter: () { numEntrances += 1; }, |
| onExit: () { numExits += 1; }, |
| ), |
| ), |
| ); |
| |
| await gesture.moveTo(tester.getCenter(find.byType(Text))); |
| await tester.pumpAndSettle(); |
| expect(numEntrances, equals(1)); |
| expect(numExits, equals(0)); |
| expect(find.text('HOVERING'), findsOneWidget); |
| |
| await tester.pumpWidget( |
| Center( |
| child: HoverFeedback( |
| key: feedbackKey, |
| onEnter: () { numEntrances += 1; }, |
| onExit: () { numExits += 1; }, |
| ), |
| ), |
| ); |
| await tester.pump(); |
| expect(numEntrances, equals(1)); |
| expect(numExits, equals(0)); |
| await tester.pumpWidget( |
| Container(), |
| ); |
| await tester.pump(); |
| expect(numEntrances, equals(1)); |
| expect(numExits, equals(0)); |
| }); |
| |
| testWidgets('Exit event when unplugging mouse should have a position', (WidgetTester tester) async { |
| final List<PointerEnterEvent> enter = <PointerEnterEvent>[]; |
| final List<PointerHoverEvent> hover = <PointerHoverEvent>[]; |
| final List<PointerExitEvent> exit = <PointerExitEvent>[]; |
| |
| await tester.pumpWidget( |
| Center( |
| child: MouseRegion( |
| onEnter: (PointerEnterEvent e) => enter.add(e), |
| onHover: (PointerHoverEvent e) => hover.add(e), |
| onExit: (PointerExitEvent e) => exit.add(e), |
| child: const SizedBox( |
| height: 100.0, |
| width: 100.0, |
| ), |
| ), |
| ), |
| ); |
| |
| // Plug-in a mouse and move it to the center of the container. |
| TestGesture? gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| await gesture.addPointer(location: Offset.zero); |
| addTearDown(() => gesture?.removePointer()); |
| await tester.pumpAndSettle(); |
| await gesture.moveTo(tester.getCenter(find.byType(SizedBox))); |
| |
| expect(enter.length, 1); |
| expect(enter.single.position, const Offset(400.0, 300.0)); |
| expect(hover.length, 1); |
| expect(hover.single.position, const Offset(400.0, 300.0)); |
| expect(exit.length, 0); |
| |
| enter.clear(); |
| hover.clear(); |
| exit.clear(); |
| |
| // Unplug the mouse. |
| await gesture.removePointer(); |
| gesture = null; |
| await tester.pumpAndSettle(); |
| |
| expect(enter.length, 0); |
| expect(hover.length, 0); |
| expect(exit.length, 1); |
| expect(exit.single.position, const Offset(400.0, 300.0)); |
| expect(exit.single.delta, Offset.zero); |
| }); |
| |
| testWidgets('detects pointer enter with closure arguments', (WidgetTester tester) async { |
| await tester.pumpWidget(_HoverClientWithClosures()); |
| expect(find.text('not hovering'), findsOneWidget); |
| |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| addTearDown(gesture.removePointer); |
| await gesture.addPointer(); |
| // Move to a position out of MouseRegion |
| await gesture.moveTo(tester.getBottomRight(find.byType(MouseRegion)) + const Offset(10, -10)); |
| await tester.pumpAndSettle(); |
| expect(find.text('not hovering'), findsOneWidget); |
| |
| // Move into MouseRegion |
| await gesture.moveBy(const Offset(-20, 0)); |
| await tester.pumpAndSettle(); |
| expect(find.text('HOVERING'), findsOneWidget); |
| }); |
| |
| testWidgets('MouseRegion paints child once and only once when MouseRegion is inactive', (WidgetTester tester) async { |
| int paintCount = 0; |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: MouseRegion( |
| onEnter: (PointerEnterEvent e) {}, |
| child: CustomPaint( |
| painter: _DelegatedPainter(onPaint: () { paintCount += 1; }), |
| child: const Text('123'), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(paintCount, 1); |
| }); |
| |
| testWidgets('MouseRegion paints child once and only once when MouseRegion is active', (WidgetTester tester) async { |
| int paintCount = 0; |
| |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| await gesture.addPointer(); |
| addTearDown(gesture.removePointer); |
| |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: MouseRegion( |
| onEnter: (PointerEnterEvent e) {}, |
| child: CustomPaint( |
| painter: _DelegatedPainter(onPaint: () { paintCount += 1; }), |
| child: const Text('123'), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(paintCount, 1); |
| }); |
| |
| testWidgets('A MouseRegion mounted under the pointer should should take effect in the next postframe', (WidgetTester tester) async { |
| bool hovered = false; |
| |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| await gesture.addPointer(location: const Offset(5, 5)); |
| addTearDown(gesture.removePointer); |
| |
| await tester.pumpWidget( |
| StatefulBuilder(builder: (BuildContext context, StateSetter setState) { |
| return _ColumnContainer( |
| children: <Widget>[ |
| Text(hovered ? 'hover outer' : 'unhover outer'), |
| ], |
| ); |
| }), |
| ); |
| |
| expect(find.text('unhover outer'), findsOneWidget); |
| |
| await tester.pumpWidget( |
| StatefulBuilder(builder: (BuildContext context, StateSetter setState) { |
| return _ColumnContainer( |
| children: <Widget>[ |
| HoverClient( |
| onHover: (bool value) { setState(() { hovered = value; }); }, |
| child: Text(hovered ? 'hover inner' : 'unhover inner'), |
| ), |
| Text(hovered ? 'hover outer' : 'unhover outer'), |
| ], |
| ); |
| }), |
| ); |
| |
| expect(find.text('unhover outer'), findsOneWidget); |
| expect(find.text('unhover inner'), findsOneWidget); |
| |
| await tester.pump(); |
| |
| expect(find.text('hover outer'), findsOneWidget); |
| expect(find.text('hover inner'), findsOneWidget); |
| expect(tester.binding.hasScheduledFrame, isFalse); |
| }); |
| |
| testWidgets('A MouseRegion unmounted under the pointer should not trigger state change', (WidgetTester tester) async { |
| bool hovered = true; |
| |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| await gesture.addPointer(location: const Offset(5, 5)); |
| addTearDown(gesture.removePointer); |
| |
| await tester.pumpWidget( |
| StatefulBuilder(builder: (BuildContext context, StateSetter setState) { |
| return _ColumnContainer( |
| children: <Widget>[ |
| HoverClient( |
| onHover: (bool value) { setState(() { hovered = value; }); }, |
| child: Text(hovered ? 'hover inner' : 'unhover inner'), |
| ), |
| Text(hovered ? 'hover outer' : 'unhover outer'), |
| ], |
| ); |
| }), |
| ); |
| |
| expect(find.text('hover outer'), findsOneWidget); |
| expect(find.text('hover inner'), findsOneWidget); |
| expect(tester.binding.hasScheduledFrame, isTrue); |
| |
| await tester.pump(); |
| expect(find.text('hover outer'), findsOneWidget); |
| expect(find.text('hover inner'), findsOneWidget); |
| expect(tester.binding.hasScheduledFrame, isFalse); |
| |
| await tester.pumpWidget( |
| StatefulBuilder(builder: (BuildContext context, StateSetter setState) { |
| return _ColumnContainer( |
| children: <Widget> [ |
| Text(hovered ? 'hover outer' : 'unhover outer'), |
| ], |
| ); |
| }), |
| ); |
| |
| expect(find.text('hover outer'), findsOneWidget); |
| expect(tester.binding.hasScheduledFrame, isFalse); |
| }); |
| |
| testWidgets('A MouseRegion moved into the mouse should take effect in the next postframe', (WidgetTester tester) async { |
| bool hovered = false; |
| final List<bool> logHovered = <bool>[]; |
| bool moved = false; |
| late StateSetter mySetState; |
| |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| await gesture.addPointer(location: const Offset(5, 5)); |
| addTearDown(gesture.removePointer); |
| |
| await tester.pumpWidget( |
| StatefulBuilder(builder: (BuildContext context, StateSetter setState) { |
| mySetState = setState; |
| return _ColumnContainer( |
| children: <Widget>[ |
| Container( |
| height: 100, |
| width: 10, |
| alignment: moved ? Alignment.topLeft : Alignment.bottomLeft, |
| child: SizedBox( |
| height: 10, |
| width: 10, |
| child: HoverClient( |
| onHover: (bool value) { |
| setState(() { hovered = value; }); |
| logHovered.add(value); |
| }, |
| child: Text(hovered ? 'hover inner' : 'unhover inner'), |
| ), |
| ), |
| ), |
| Text(hovered ? 'hover outer' : 'unhover outer'), |
| ], |
| ); |
| }), |
| ); |
| |
| expect(find.text('unhover inner'), findsOneWidget); |
| expect(find.text('unhover outer'), findsOneWidget); |
| expect(logHovered, isEmpty); |
| expect(tester.binding.hasScheduledFrame, isFalse); |
| |
| mySetState(() { moved = true; }); |
| // The first frame is for the widget movement to take effect. |
| await tester.pump(); |
| expect(find.text('unhover inner'), findsOneWidget); |
| expect(find.text('unhover outer'), findsOneWidget); |
| expect(logHovered, <bool>[true]); |
| logHovered.clear(); |
| |
| // The second frame is for the mouse hover to take effect. |
| await tester.pump(); |
| expect(find.text('hover inner'), findsOneWidget); |
| expect(find.text('hover outer'), findsOneWidget); |
| expect(logHovered, isEmpty); |
| expect(tester.binding.hasScheduledFrame, isFalse); |
| }); |
| |
| group('MouseRegion respects opacity:', () { |
| |
| // A widget that contains 3 MouseRegions: |
| // y |
| // —————————————————————— 0 |
| // | ——————————— A | 20 |
| // | | B | | |
| // | | ——————————— | 50 |
| // | | | C | | |
| // | ——————| | | 100 |
| // | | | | |
| // | ——————————— | 130 |
| // —————————————————————— 150 |
| // x 0 20 50 100 130 150 |
| Widget tripleRegions({bool? opaqueC, required void Function(String) addLog}) { |
| // Same as MouseRegion, but when opaque is null, use the default value. |
| Widget mouseRegionWithOptionalOpaque({ |
| void Function(PointerEnterEvent e)? onEnter, |
| void Function(PointerHoverEvent e)? onHover, |
| void Function(PointerExitEvent e)? onExit, |
| Widget? child, |
| bool? opaque, |
| }) { |
| if (opaque == null) { |
| return MouseRegion(onEnter: onEnter, onHover: onHover, onExit: onExit, child: child); |
| } |
| return MouseRegion(onEnter: onEnter, onHover: onHover, onExit: onExit, opaque: opaque, child: child); |
| } |
| |
| return Directionality( |
| textDirection: TextDirection.ltr, |
| child: Align( |
| alignment: Alignment.topLeft, |
| child: MouseRegion( |
| onEnter: (PointerEnterEvent e) { addLog('enterA'); }, |
| onHover: (PointerHoverEvent e) { addLog('hoverA'); }, |
| onExit: (PointerExitEvent e) { addLog('exitA'); }, |
| child: SizedBox( |
| width: 150, |
| height: 150, |
| child: Stack( |
| children: <Widget>[ |
| Positioned( |
| left: 20, |
| top: 20, |
| width: 80, |
| height: 80, |
| child: MouseRegion( |
| onEnter: (PointerEnterEvent e) { addLog('enterB'); }, |
| onHover: (PointerHoverEvent e) { addLog('hoverB'); }, |
| onExit: (PointerExitEvent e) { addLog('exitB'); }, |
| ), |
| ), |
| Positioned( |
| left: 50, |
| top: 50, |
| width: 80, |
| height: 80, |
| child: mouseRegionWithOptionalOpaque( |
| opaque: opaqueC, |
| onEnter: (PointerEnterEvent e) { addLog('enterC'); }, |
| onHover: (PointerHoverEvent e) { addLog('hoverC'); }, |
| onExit: (PointerExitEvent e) { addLog('exitC'); }, |
| ), |
| ), |
| ], |
| ), |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| testWidgets('a transparent one should allow MouseRegions behind it to receive pointers', (WidgetTester tester) async { |
| final List<String> logs = <String>[]; |
| await tester.pumpWidget(tripleRegions( |
| opaqueC: false, |
| addLog: (String log) => logs.add(log), |
| )); |
| |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| await gesture.addPointer(); |
| addTearDown(gesture.removePointer); |
| await tester.pumpAndSettle(); |
| |
| // Move to the overlapping area. |
| await gesture.moveTo(const Offset(75, 75)); |
| await tester.pumpAndSettle(); |
| expect(logs, <String>['enterA', 'enterB', 'enterC', 'hoverC', 'hoverB', 'hoverA']); |
| logs.clear(); |
| |
| // Move to the B only area. |
| await gesture.moveTo(const Offset(25, 75)); |
| await tester.pumpAndSettle(); |
| expect(logs, <String>['exitC', 'hoverB', 'hoverA']); |
| logs.clear(); |
| |
| // Move back to the overlapping area. |
| await gesture.moveTo(const Offset(75, 75)); |
| await tester.pumpAndSettle(); |
| expect(logs, <String>['enterC', 'hoverC', 'hoverB', 'hoverA']); |
| logs.clear(); |
| |
| // Move to the C only area. |
| await gesture.moveTo(const Offset(125, 75)); |
| await tester.pumpAndSettle(); |
| expect(logs, <String>['exitB', 'hoverC', 'hoverA']); |
| logs.clear(); |
| |
| // Move back to the overlapping area. |
| await gesture.moveTo(const Offset(75, 75)); |
| await tester.pumpAndSettle(); |
| expect(logs, <String>['enterB', 'hoverC', 'hoverB', 'hoverA']); |
| logs.clear(); |
| |
| // Move out. |
| await gesture.moveTo(const Offset(160, 160)); |
| await tester.pumpAndSettle(); |
| expect(logs, <String>['exitC', 'exitB', 'exitA']); |
| }); |
| |
| testWidgets('an opaque one should prevent MouseRegions behind it receiving pointers', (WidgetTester tester) async { |
| final List<String> logs = <String>[]; |
| await tester.pumpWidget(tripleRegions( |
| opaqueC: true, |
| addLog: (String log) => logs.add(log), |
| )); |
| |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| await gesture.addPointer(); |
| addTearDown(gesture.removePointer); |
| await tester.pumpAndSettle(); |
| |
| // Move to the overlapping area. |
| await gesture.moveTo(const Offset(75, 75)); |
| await tester.pumpAndSettle(); |
| expect(logs, <String>['enterA', 'enterC', 'hoverC', 'hoverA']); |
| logs.clear(); |
| |
| // Move to the B only area. |
| await gesture.moveTo(const Offset(25, 75)); |
| await tester.pumpAndSettle(); |
| expect(logs, <String>['exitC', 'enterB', 'hoverB', 'hoverA']); |
| logs.clear(); |
| |
| // Move back to the overlapping area. |
| await gesture.moveTo(const Offset(75, 75)); |
| await tester.pumpAndSettle(); |
| expect(logs, <String>['exitB', 'enterC', 'hoverC', 'hoverA']); |
| logs.clear(); |
| |
| // Move to the C only area. |
| await gesture.moveTo(const Offset(125, 75)); |
| await tester.pumpAndSettle(); |
| expect(logs, <String>['hoverC', 'hoverA']); |
| logs.clear(); |
| |
| // Move back to the overlapping area. |
| await gesture.moveTo(const Offset(75, 75)); |
| await tester.pumpAndSettle(); |
| expect(logs, <String>['hoverC', 'hoverA']); |
| logs.clear(); |
| |
| // Move out. |
| await gesture.moveTo(const Offset(160, 160)); |
| await tester.pumpAndSettle(); |
| expect(logs, <String>['exitC', 'exitA']); |
| }); |
| |
| testWidgets('opaque should default to true', (WidgetTester tester) async { |
| final List<String> logs = <String>[]; |
| await tester.pumpWidget(tripleRegions( |
| addLog: (String log) => logs.add(log), |
| )); |
| |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| await gesture.addPointer(); |
| addTearDown(gesture.removePointer); |
| await tester.pumpAndSettle(); |
| |
| // Move to the overlapping area. |
| await gesture.moveTo(const Offset(75, 75)); |
| await tester.pumpAndSettle(); |
| expect(logs, <String>['enterA', 'enterC', 'hoverC', 'hoverA']); |
| logs.clear(); |
| |
| // Move out. |
| await gesture.moveTo(const Offset(160, 160)); |
| await tester.pumpAndSettle(); |
| expect(logs, <String>['exitC', 'exitA']); |
| }); |
| }); |
| |
| testWidgets('an empty opaque MouseRegion is effective', (WidgetTester tester) async { |
| bool bottomRegionIsHovered = false; |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: Stack( |
| children: <Widget>[ |
| Align( |
| alignment: Alignment.topLeft, |
| child: MouseRegion( |
| onEnter: (_) { bottomRegionIsHovered = true; }, |
| onHover: (_) { bottomRegionIsHovered = true; }, |
| onExit: (_) { bottomRegionIsHovered = true; }, |
| child: const SizedBox( |
| width: 10, |
| height: 10, |
| ), |
| ), |
| ), |
| const MouseRegion(), |
| ], |
| ), |
| ), |
| ); |
| |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| await gesture.addPointer(location: const Offset(20, 20)); |
| addTearDown(gesture.removePointer); |
| |
| await gesture.moveTo(const Offset(5, 5)); |
| await tester.pump(); |
| await gesture.moveTo(const Offset(20, 20)); |
| await tester.pump(); |
| expect(bottomRegionIsHovered, isFalse); |
| }); |
| |
| testWidgets("Changing MouseRegion's callbacks is effective and doesn't repaint", (WidgetTester tester) async { |
| final List<String> logs = <String>[]; |
| const Key key = ValueKey<int>(1); |
| |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| await gesture.addPointer(location: const Offset(20, 20)); |
| addTearDown(gesture.removePointer); |
| |
| await tester.pumpWidget(_Scaffold( |
| topLeft: SizedBox( |
| height: 10, |
| width: 10, |
| child: MouseRegion( |
| onEnter: (_) { logs.add('enter1'); }, |
| onHover: (_) { logs.add('hover1'); }, |
| onExit: (_) { logs.add('exit1'); }, |
| child: CustomPaint( |
| painter: _DelegatedPainter(onPaint: () { logs.add('paint'); }, key: key), |
| ), |
| ), |
| ), |
| )); |
| expect(logs, <String>['paint']); |
| logs.clear(); |
| |
| await gesture.moveTo(const Offset(5, 5)); |
| expect(logs, <String>['enter1', 'hover1']); |
| logs.clear(); |
| |
| await tester.pumpWidget(_Scaffold( |
| topLeft: SizedBox( |
| height: 10, |
| width: 10, |
| child: MouseRegion( |
| onEnter: (_) { logs.add('enter2'); }, |
| onHover: (_) { logs.add('hover2'); }, |
| onExit: (_) { logs.add('exit2'); }, |
| child: CustomPaint( |
| painter: _DelegatedPainter(onPaint: () { logs.add('paint'); }, key: key), |
| ), |
| ), |
| ), |
| )); |
| expect(logs, isEmpty); |
| |
| await gesture.moveTo(const Offset(6, 6)); |
| expect(logs, <String>['hover2']); |
| logs.clear(); |
| |
| // Compare: It repaints if the MouseRegion is deactivated. |
| await tester.pumpWidget(_Scaffold( |
| topLeft: SizedBox( |
| height: 10, |
| width: 10, |
| child: MouseRegion( |
| opaque: false, |
| child: CustomPaint( |
| painter: _DelegatedPainter(onPaint: () { logs.add('paint'); }, key: key), |
| ), |
| ), |
| ), |
| )); |
| expect(logs, <String>['paint']); |
| }); |
| |
| testWidgets('Changing MouseRegion.opaque is effective and repaints', (WidgetTester tester) async { |
| final List<String> logs = <String>[]; |
| |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| await gesture.addPointer(location: const Offset(5, 5)); |
| addTearDown(gesture.removePointer); |
| |
| void _handleHover(PointerHoverEvent _) {} |
| void _handlePaintChild() { logs.add('paint'); } |
| |
| await tester.pumpWidget(_Scaffold( |
| topLeft: SizedBox( |
| height: 10, |
| width: 10, |
| child: MouseRegion( |
| onHover: _handleHover, |
| child: CustomPaint(painter: _DelegatedPainter(onPaint: _handlePaintChild)), |
| ), |
| ), |
| background: MouseRegion(onEnter: (_) { logs.add('hover-enter'); }), |
| )); |
| expect(logs, <String>['paint']); |
| logs.clear(); |
| |
| expect(logs, isEmpty); |
| logs.clear(); |
| |
| await tester.pumpWidget(_Scaffold( |
| topLeft: SizedBox( |
| height: 10, |
| width: 10, |
| child: MouseRegion( |
| opaque: false, |
| // Dummy callback so that MouseRegion stays affective after opaque |
| // turns false. |
| onHover: _handleHover, |
| child: CustomPaint(painter: _DelegatedPainter(onPaint: _handlePaintChild)), |
| ), |
| ), |
| background: MouseRegion(onEnter: (_) { logs.add('hover-enter'); }), |
| )); |
| |
| expect(logs, <String>['paint', 'hover-enter']); |
| }); |
| |
| testWidgets('Changing MouseRegion.cursor is effective and repaints', (WidgetTester tester) async { |
| final List<String> logPaints = <String>[]; |
| final List<String> logEnters = <String>[]; |
| |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| await gesture.addPointer(location: const Offset(100, 100)); |
| addTearDown(gesture.removePointer); |
| |
| void onPaintChild() { logPaints.add('paint'); } |
| |
| await tester.pumpWidget(_Scaffold( |
| topLeft: SizedBox( |
| height: 10, |
| width: 10, |
| child: MouseRegion( |
| cursor: SystemMouseCursors.forbidden, |
| onEnter: (_) { logEnters.add('enter'); }, |
| child: CustomPaint(painter: _DelegatedPainter(onPaint: onPaintChild)), |
| ), |
| ), |
| )); |
| await gesture.moveTo(const Offset(5, 5)); |
| |
| expect(logPaints, <String>['paint']); |
| expect(RendererBinding.instance!.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.forbidden); |
| expect(logEnters, <String>['enter']); |
| logPaints.clear(); |
| logEnters.clear(); |
| |
| await tester.pumpWidget(_Scaffold( |
| topLeft: SizedBox( |
| height: 10, |
| width: 10, |
| child: MouseRegion( |
| cursor: SystemMouseCursors.text, |
| onEnter: (_) { logEnters.add('enter'); }, |
| child: CustomPaint(painter: _DelegatedPainter(onPaint: onPaintChild)), |
| ), |
| ), |
| )); |
| |
| expect(logPaints, <String>['paint']); |
| expect(RendererBinding.instance!.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text); |
| expect(logEnters, isEmpty); |
| logPaints.clear(); |
| logEnters.clear(); |
| }); |
| |
| testWidgets('Changing whether MouseRegion.cursor is null is effective and repaints', (WidgetTester tester) async { |
| final List<String> logEnters = <String>[]; |
| final List<String> logPaints = <String>[]; |
| |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| await gesture.addPointer(location: const Offset(100, 100)); |
| addTearDown(gesture.removePointer); |
| |
| void onPaintChild() { logPaints.add('paint'); } |
| |
| await tester.pumpWidget(_Scaffold( |
| topLeft: SizedBox( |
| height: 10, |
| width: 10, |
| child: MouseRegion( |
| cursor: SystemMouseCursors.forbidden, |
| child: MouseRegion( |
| cursor: SystemMouseCursors.text, |
| onEnter: (_) { logEnters.add('enter'); }, |
| child: CustomPaint(painter: _DelegatedPainter(onPaint: onPaintChild)), |
| ), |
| ), |
| ), |
| )); |
| await gesture.moveTo(const Offset(5, 5)); |
| |
| expect(logPaints, <String>['paint']); |
| expect(logEnters, <String>['enter']); |
| expect(RendererBinding.instance!.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text); |
| logPaints.clear(); |
| logEnters.clear(); |
| |
| await tester.pumpWidget(_Scaffold( |
| topLeft: SizedBox( |
| height: 10, |
| width: 10, |
| child: MouseRegion( |
| cursor: SystemMouseCursors.forbidden, |
| child: MouseRegion( |
| onEnter: (_) { logEnters.add('enter'); }, |
| child: CustomPaint(painter: _DelegatedPainter(onPaint: onPaintChild)), |
| ), |
| ), |
| ), |
| )); |
| |
| expect(logPaints, <String>['paint']); |
| expect(logEnters, isEmpty); |
| expect(RendererBinding.instance!.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.forbidden); |
| logPaints.clear(); |
| logEnters.clear(); |
| |
| await tester.pumpWidget(_Scaffold( |
| topLeft: SizedBox( |
| height: 10, |
| width: 10, |
| child: MouseRegion( |
| cursor: SystemMouseCursors.forbidden, |
| child: MouseRegion( |
| cursor: SystemMouseCursors.text, |
| child: CustomPaint(painter: _DelegatedPainter(onPaint: onPaintChild)), |
| ), |
| ), |
| ), |
| )); |
| |
| expect(logPaints, <String>['paint']); |
| expect(RendererBinding.instance!.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text); |
| expect(logEnters, isEmpty); |
| logPaints.clear(); |
| logEnters.clear(); |
| }); |
| |
| testWidgets('Does not trigger side effects during a reparent', (WidgetTester tester) async { |
| final List<String> logEnters = <String>[]; |
| final List<String> logExits = <String>[]; |
| final List<String> logCursors = <String>[]; |
| |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| await gesture.addPointer(location: const Offset(100, 100)); |
| addTearDown(gesture.removePointer); |
| tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.mouseCursor, (_) async { |
| logCursors.add('cursor'); |
| }); |
| |
| final GlobalKey key = GlobalKey(); |
| |
| // Pump a row of 2 SizedBox's, each taking 50px of width. |
| await tester.pumpWidget(_Scaffold( |
| topLeft: SizedBox( |
| width: 100, |
| height: 50, |
| child: Row( |
| children: <Widget>[ |
| SizedBox( |
| width: 50, |
| height: 50, |
| child: MouseRegion( |
| key: key, |
| onEnter: (_) { logEnters.add('enter'); }, |
| onExit: (_) { logEnters.add('enter'); }, |
| cursor: SystemMouseCursors.click, |
| ), |
| ), |
| const SizedBox( |
| width: 50, |
| height: 50, |
| ), |
| ], |
| ), |
| ), |
| )); |
| |
| // Move to the mouse region inside the first box. |
| await gesture.moveTo(const Offset(40, 5)); |
| |
| expect(logEnters, <String>['enter']); |
| expect(logExits, isEmpty); |
| expect(logCursors, isNotEmpty); |
| expect(RendererBinding.instance!.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click); |
| logEnters.clear(); |
| logExits.clear(); |
| logCursors.clear(); |
| |
| // Move MouseRegion to the second box while resizing them so that the |
| // mouse is still on the MouseRegion |
| await tester.pumpWidget(_Scaffold( |
| topLeft: SizedBox( |
| width: 100, |
| height: 50, |
| child: Row( |
| children: <Widget>[ |
| const SizedBox( |
| width: 30, |
| height: 50, |
| ), |
| SizedBox( |
| width: 70, |
| height: 50, |
| child: MouseRegion( |
| key: key, |
| onEnter: (_) { logEnters.add('enter'); }, |
| onExit: (_) { logEnters.add('enter'); }, |
| cursor: SystemMouseCursors.click, |
| ), |
| ), |
| ], |
| ), |
| ), |
| )); |
| |
| expect(logEnters, isEmpty); |
| expect(logExits, isEmpty); |
| expect(logCursors, isEmpty); |
| expect(RendererBinding.instance!.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click); |
| }); |
| |
| testWidgets("RenderMouseRegion's debugFillProperties when default", (WidgetTester tester) async { |
| final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); |
| RenderMouseRegion().debugFillProperties(builder); |
| |
| final List<String> description = builder.properties.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)).map((DiagnosticsNode node) => node.toString()).toList(); |
| |
| expect(description, <String>[ |
| 'parentData: MISSING', |
| 'constraints: MISSING', |
| 'size: MISSING', |
| 'listeners: <none>', |
| ]); |
| }); |
| |
| testWidgets("RenderMouseRegion's debugFillProperties when full", (WidgetTester tester) async { |
| final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); |
| RenderMouseRegion( |
| onEnter: (PointerEnterEvent event) {}, |
| onExit: (PointerExitEvent event) {}, |
| onHover: (PointerHoverEvent event) {}, |
| cursor: SystemMouseCursors.click, |
| validForMouseTracker: false, |
| child: RenderErrorBox(), |
| ).debugFillProperties(builder); |
| |
| final List<String> description = builder.properties.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)).map((DiagnosticsNode node) => node.toString()).toList(); |
| |
| expect(description, <String>[ |
| 'parentData: MISSING', |
| 'constraints: MISSING', |
| 'size: MISSING', |
| 'listeners: enter, hover, exit', |
| 'cursor: SystemMouseCursor(click)', |
| 'invalid for MouseTracker', |
| ]); |
| }); |
| |
| testWidgets('No new frames are scheduled when mouse moves without triggering callbacks', (WidgetTester tester) async { |
| await tester.pumpWidget(Center( |
| child: MouseRegion( |
| child: const SizedBox( |
| width: 100.0, |
| height: 100.0, |
| ), |
| onEnter: (PointerEnterEvent details) {}, |
| onHover: (PointerHoverEvent details) {}, |
| onExit: (PointerExitEvent details) {}, |
| ), |
| )); |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| await gesture.addPointer(location: const Offset(400.0, 300.0)); |
| addTearDown(gesture.removePointer); |
| await tester.pumpAndSettle(); |
| await gesture.moveBy(const Offset(10.0, 10.0)); |
| expect(tester.binding.hasScheduledFrame, isFalse); |
| }); |
| |
| // Regression test for https://github.com/flutter/flutter/issues/67044 |
| testWidgets('Handle mouse events should ignore the detached MouseTrackerAnnotation', (WidgetTester tester) async { |
| await tester.pumpWidget(MaterialApp( |
| home: Center( |
| child: Draggable<int>( |
| feedback: Container(width: 20, height: 20, color: Colors.blue), |
| childWhenDragging: Container(width: 20, height: 20, color: Colors.yellow), |
| child: ElevatedButton(child: const Text('Drag me'), onPressed: (){}), |
| ), |
| ), |
| )); |
| |
| // Long press the button with mouse. |
| final Offset textFieldPos = tester.getCenter(find.byType(Text)); |
| final TestGesture gesture = await tester.startGesture( |
| textFieldPos, |
| kind: PointerDeviceKind.mouse, |
| ); |
| addTearDown(gesture.removePointer); |
| await tester.pump(const Duration(seconds: 2)); |
| await tester.pumpAndSettle(); |
| |
| // Drag the Draggable Widget will replace the child with [childWhenDragging]. |
| await gesture.moveBy(const Offset(10.0, 10.0)); |
| await tester.pump(); // Trigger detach the button. |
| |
| // Continue drag mouse should not trigger any assert. |
| await gesture.moveBy(const Offset(10.0, 10.0)); |
| expect(tester.takeException(), isNull); |
| }); |
| } |
| |
| // Render widget `topLeft` at the top-left corner, stacking on top of the widget |
| // `background`. |
| class _Scaffold extends StatelessWidget { |
| const _Scaffold({this.topLeft, this.background}); |
| |
| final Widget? topLeft; |
| final Widget? background; |
| |
| @override |
| Widget build(BuildContext context) { |
| return Directionality( |
| textDirection: TextDirection.ltr, |
| child: Stack( |
| children: <Widget>[ |
| if (background != null) background!, |
| Align( |
| alignment: Alignment.topLeft, |
| child: topLeft, |
| ), |
| ], |
| ), |
| ); |
| } |
| } |
| |
| class _DelegatedPainter extends CustomPainter { |
| _DelegatedPainter({this.key, required this.onPaint}); |
| final Key? key; |
| final VoidCallback onPaint; |
| |
| @override |
| void paint(Canvas canvas, Size size) { |
| onPaint(); |
| } |
| |
| @override |
| bool shouldRepaint(CustomPainter oldDelegate) => |
| !(oldDelegate is _DelegatedPainter && key == oldDelegate.key); |
| } |
| |
| class _HoverClientWithClosures extends StatefulWidget { |
| @override |
| _HoverClientWithClosuresState createState() => _HoverClientWithClosuresState(); |
| } |
| |
| class _HoverClientWithClosuresState extends State<_HoverClientWithClosures> { |
| bool _hovering = false; |
| |
| @override |
| Widget build(BuildContext context) { |
| return Directionality( |
| textDirection: TextDirection.ltr, |
| child: MouseRegion( |
| onEnter: (PointerEnterEvent _) { |
| setState(() { |
| _hovering = true; |
| }); |
| }, |
| onExit: (PointerExitEvent _) { |
| setState(() { |
| _hovering = false; |
| }); |
| }, |
| child: Text(_hovering ? 'HOVERING' : 'not hovering'), |
| ), |
| ); |
| } |
| } |
| |
| // A column that aligns to the top left. |
| class _ColumnContainer extends StatelessWidget { |
| const _ColumnContainer({ |
| required this.children, |
| }) : assert(children != null); |
| |
| final List<Widget> children; |
| |
| @override |
| Widget build(BuildContext context) { |
| return Directionality( |
| textDirection: TextDirection.ltr, |
| child: Column( |
| crossAxisAlignment: CrossAxisAlignment.start, |
| children: children, |
| ), |
| ); |
| } |
| } |