1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | import 'package:flutter/foundation.dart'; |
6 | import 'package:flutter/gestures.dart'; |
7 | import 'package:flutter/material.dart'; |
8 | import 'package:flutter/rendering.dart'; |
9 | import 'package:flutter/services.dart'; |
10 | import 'package:flutter_test/flutter_test.dart'; |
11 | |
12 | class HoverClient extends StatefulWidget { |
13 | const HoverClient({super.key, this.onHover, this.child, this.onEnter, this.onExit}); |
14 | |
15 | final ValueChanged<bool>? onHover; |
16 | final Widget? child; |
17 | final VoidCallback? onEnter; |
18 | final VoidCallback? onExit; |
19 | |
20 | @override |
21 | HoverClientState createState() => HoverClientState(); |
22 | } |
23 | |
24 | class HoverClientState extends State<HoverClient> { |
25 | void _onExit(PointerExitEvent details) { |
26 | widget.onExit?.call(); |
27 | widget.onHover?.call(false); |
28 | } |
29 | |
30 | void _onEnter(PointerEnterEvent details) { |
31 | widget.onEnter?.call(); |
32 | widget.onHover?.call(true); |
33 | } |
34 | |
35 | @override |
36 | Widget build(BuildContext context) { |
37 | return MouseRegion(onEnter: _onEnter, onExit: _onExit, child: widget.child); |
38 | } |
39 | } |
40 | |
41 | class HoverFeedback extends StatefulWidget { |
42 | const HoverFeedback({super.key, this.onEnter, this.onExit}); |
43 | |
44 | final VoidCallback? onEnter; |
45 | final VoidCallback? onExit; |
46 | |
47 | @override |
48 | State<HoverFeedback> createState() => _HoverFeedbackState(); |
49 | } |
50 | |
51 | class _HoverFeedbackState extends State<HoverFeedback> { |
52 | bool _hovering = false; |
53 | |
54 | @override |
55 | Widget build(BuildContext context) { |
56 | return Directionality( |
57 | textDirection: TextDirection.ltr, |
58 | child: HoverClient( |
59 | onHover: (bool hovering) => setState(() => _hovering = hovering), |
60 | onEnter: widget.onEnter, |
61 | onExit: widget.onExit, |
62 | child: Text(_hovering ? 'HOVERING' : 'not hovering' ), |
63 | ), |
64 | ); |
65 | } |
66 | } |
67 | |
68 | void main() { |
69 | // Regression test for https://github.com/flutter/flutter/issues/73330 |
70 | testWidgets('hitTestBehavior test - HitTestBehavior.deferToChild/opaque' , ( |
71 | WidgetTester tester, |
72 | ) async { |
73 | bool onEnter = false; |
74 | await tester.pumpWidget( |
75 | Center( |
76 | child: MouseRegion( |
77 | hitTestBehavior: HitTestBehavior.deferToChild, |
78 | onEnter: (_) => onEnter = true, |
79 | ), |
80 | ), |
81 | ); |
82 | |
83 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
84 | await gesture.addPointer(location: Offset.zero); |
85 | await tester.pump(); |
86 | |
87 | // The child is null, so `onEnter` does not trigger. |
88 | expect(onEnter, false); |
89 | |
90 | // Update to the default value `HitTestBehavior.opaque` |
91 | await tester.pumpWidget(Center(child: MouseRegion(onEnter: (_) => onEnter = true))); |
92 | |
93 | expect(onEnter, true); |
94 | }); |
95 | |
96 | testWidgets('hitTestBehavior test - HitTestBehavior.deferToChild and non-opaque' , ( |
97 | WidgetTester tester, |
98 | ) async { |
99 | bool onEnterRegion1 = false; |
100 | bool onEnterRegion2 = false; |
101 | await tester.pumpWidget( |
102 | Directionality( |
103 | textDirection: TextDirection.ltr, |
104 | child: Stack( |
105 | children: <Widget>[ |
106 | SizedBox( |
107 | width: 50.0, |
108 | height: 50.0, |
109 | child: MouseRegion(onEnter: (_) => onEnterRegion1 = true), |
110 | ), |
111 | SizedBox( |
112 | width: 50.0, |
113 | height: 50.0, |
114 | child: MouseRegion( |
115 | opaque: false, |
116 | hitTestBehavior: HitTestBehavior.deferToChild, |
117 | onEnter: (_) => onEnterRegion2 = true, |
118 | child: Container( |
119 | color: const Color.fromARGB(0xff, 0xff, 0x10, 0x19), |
120 | width: 50.0, |
121 | height: 50.0, |
122 | ), |
123 | ), |
124 | ), |
125 | ], |
126 | ), |
127 | ), |
128 | ); |
129 | |
130 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
131 | await gesture.addPointer(location: Offset.zero); |
132 | await tester.pump(); |
133 | |
134 | expect(onEnterRegion2, true); |
135 | expect(onEnterRegion1, true); |
136 | }); |
137 | |
138 | testWidgets('hitTestBehavior test - HitTestBehavior.translucent' , (WidgetTester tester) async { |
139 | bool onEnterRegion1 = false; |
140 | bool onEnterRegion2 = false; |
141 | await tester.pumpWidget( |
142 | Directionality( |
143 | textDirection: TextDirection.ltr, |
144 | child: Stack( |
145 | children: <Widget>[ |
146 | SizedBox( |
147 | width: 50.0, |
148 | height: 50.0, |
149 | child: MouseRegion(onEnter: (_) => onEnterRegion1 = true), |
150 | ), |
151 | SizedBox( |
152 | width: 50.0, |
153 | height: 50.0, |
154 | child: MouseRegion( |
155 | hitTestBehavior: HitTestBehavior.translucent, |
156 | onEnter: (_) => onEnterRegion2 = true, |
157 | ), |
158 | ), |
159 | ], |
160 | ), |
161 | ), |
162 | ); |
163 | |
164 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
165 | await gesture.addPointer(location: Offset.zero); |
166 | await tester.pump(); |
167 | |
168 | expect(onEnterRegion2, true); |
169 | expect(onEnterRegion1, true); |
170 | }); |
171 | |
172 | testWidgets('onEnter and onExit can be triggered with mouse buttons pressed' , ( |
173 | WidgetTester tester, |
174 | ) async { |
175 | PointerEnterEvent? enter; |
176 | PointerExitEvent? exit; |
177 | await tester.pumpWidget( |
178 | Center( |
179 | child: MouseRegion( |
180 | child: Container( |
181 | color: const Color.fromARGB(0xff, 0xff, 0x00, 0x00), |
182 | width: 100.0, |
183 | height: 100.0, |
184 | ), |
185 | onEnter: (PointerEnterEvent details) => enter = details, |
186 | onExit: (PointerExitEvent details) => exit = details, |
187 | ), |
188 | ), |
189 | ); |
190 | |
191 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
192 | await gesture.addPointer(location: Offset.zero); |
193 | await gesture.down(Offset.zero); // Press the mouse button. |
194 | await tester.pump(); |
195 | enter = null; |
196 | exit = null; |
197 | // Trigger the enter event. |
198 | await gesture.moveTo(const Offset(400.0, 300.0)); |
199 | expect(enter, isNotNull); |
200 | expect(enter!.position, equals(const Offset(400.0, 300.0))); |
201 | expect(enter!.localPosition, equals(const Offset(50.0, 50.0))); |
202 | expect(exit, isNull); |
203 | |
204 | // Trigger the exit event. |
205 | await gesture.moveTo(const Offset(1.0, 1.0)); |
206 | expect(exit, isNotNull); |
207 | expect(exit!.position, equals(const Offset(1.0, 1.0))); |
208 | expect(exit!.localPosition, equals(const Offset(-349.0, -249.0))); |
209 | }); |
210 | |
211 | testWidgets('detects pointer enter' , (WidgetTester tester) async { |
212 | PointerEnterEvent? enter; |
213 | PointerHoverEvent? move; |
214 | PointerExitEvent? exit; |
215 | await tester.pumpWidget( |
216 | Center( |
217 | child: MouseRegion( |
218 | child: Container( |
219 | color: const Color.fromARGB(0xff, 0xff, 0x00, 0x00), |
220 | width: 100.0, |
221 | height: 100.0, |
222 | ), |
223 | onEnter: (PointerEnterEvent details) => enter = details, |
224 | onHover: (PointerHoverEvent details) => move = details, |
225 | onExit: (PointerExitEvent details) => exit = details, |
226 | ), |
227 | ), |
228 | ); |
229 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
230 | await gesture.addPointer(location: Offset.zero); |
231 | await tester.pump(); |
232 | move = null; |
233 | enter = null; |
234 | exit = null; |
235 | await gesture.moveTo(const Offset(400.0, 300.0)); |
236 | expect(move, isNotNull); |
237 | expect(move!.position, equals(const Offset(400.0, 300.0))); |
238 | expect(move!.localPosition, equals(const Offset(50.0, 50.0))); |
239 | expect(enter, isNotNull); |
240 | expect(enter!.position, equals(const Offset(400.0, 300.0))); |
241 | expect(enter!.localPosition, equals(const Offset(50.0, 50.0))); |
242 | expect(exit, isNull); |
243 | }); |
244 | |
245 | testWidgets('detects pointer exiting' , (WidgetTester tester) async { |
246 | PointerEnterEvent? enter; |
247 | PointerHoverEvent? move; |
248 | PointerExitEvent? exit; |
249 | await tester.pumpWidget( |
250 | Center( |
251 | child: MouseRegion( |
252 | child: Container( |
253 | color: const Color.fromARGB(0xff, 0xff, 0x00, 0x00), |
254 | width: 100.0, |
255 | height: 100.0, |
256 | ), |
257 | onEnter: (PointerEnterEvent details) => enter = details, |
258 | onHover: (PointerHoverEvent details) => move = details, |
259 | onExit: (PointerExitEvent details) => exit = details, |
260 | ), |
261 | ), |
262 | ); |
263 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
264 | await gesture.addPointer(location: Offset.zero); |
265 | await gesture.moveTo(const Offset(400.0, 300.0)); |
266 | await tester.pump(); |
267 | move = null; |
268 | enter = null; |
269 | exit = null; |
270 | await gesture.moveTo(const Offset(1.0, 1.0)); |
271 | expect(move, isNull); |
272 | expect(enter, isNull); |
273 | expect(exit, isNotNull); |
274 | expect(exit!.position, equals(const Offset(1.0, 1.0))); |
275 | expect(exit!.localPosition, equals(const Offset(-349.0, -249.0))); |
276 | }); |
277 | |
278 | testWidgets('triggers pointer enter when a mouse is connected' , (WidgetTester tester) async { |
279 | PointerEnterEvent? enter; |
280 | PointerHoverEvent? move; |
281 | PointerExitEvent? exit; |
282 | await tester.pumpWidget( |
283 | Center( |
284 | child: MouseRegion( |
285 | child: const SizedBox(width: 100.0, height: 100.0), |
286 | onEnter: (PointerEnterEvent details) => enter = details, |
287 | onHover: (PointerHoverEvent details) => move = details, |
288 | onExit: (PointerExitEvent details) => exit = details, |
289 | ), |
290 | ), |
291 | ); |
292 | await tester.pump(); |
293 | |
294 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
295 | await gesture.addPointer(location: const Offset(400, 300)); |
296 | expect(move, isNull); |
297 | expect(enter, isNotNull); |
298 | expect(enter!.position, equals(const Offset(400.0, 300.0))); |
299 | expect(enter!.localPosition, equals(const Offset(50.0, 50.0))); |
300 | expect(exit, isNull); |
301 | }); |
302 | |
303 | testWidgets('triggers pointer exit when a mouse is disconnected' , (WidgetTester tester) async { |
304 | PointerEnterEvent? enter; |
305 | PointerHoverEvent? move; |
306 | PointerExitEvent? exit; |
307 | await tester.pumpWidget( |
308 | Center( |
309 | child: MouseRegion( |
310 | child: const SizedBox(width: 100.0, height: 100.0), |
311 | onEnter: (PointerEnterEvent details) => enter = details, |
312 | onHover: (PointerHoverEvent details) => move = details, |
313 | onExit: (PointerExitEvent details) => exit = details, |
314 | ), |
315 | ), |
316 | ); |
317 | await tester.pump(); |
318 | |
319 | TestGesture? gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
320 | await gesture.addPointer(location: const Offset(400, 300)); |
321 | addTearDown(() => gesture?.removePointer); |
322 | await tester.pump(); |
323 | move = null; |
324 | enter = null; |
325 | exit = null; |
326 | await gesture.removePointer(); |
327 | gesture = null; |
328 | expect(move, isNull); |
329 | expect(enter, isNull); |
330 | expect(exit, isNotNull); |
331 | expect(exit!.position, equals(const Offset(400.0, 300.0))); |
332 | expect(exit!.localPosition, equals(const Offset(50.0, 50.0))); |
333 | exit = null; |
334 | await tester.pump(); |
335 | expect(move, isNull); |
336 | expect(enter, isNull); |
337 | expect(exit, isNull); |
338 | }); |
339 | |
340 | testWidgets('triggers pointer enter when widget appears' , (WidgetTester tester) async { |
341 | PointerEnterEvent? enter; |
342 | PointerHoverEvent? move; |
343 | PointerExitEvent? exit; |
344 | await tester.pumpWidget(const Center(child: SizedBox(width: 100.0, height: 100.0))); |
345 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
346 | await gesture.addPointer(location: Offset.zero); |
347 | await gesture.moveTo(const Offset(400.0, 300.0)); |
348 | await tester.pump(); |
349 | expect(enter, isNull); |
350 | expect(move, isNull); |
351 | expect(exit, isNull); |
352 | await tester.pumpWidget( |
353 | Center( |
354 | child: MouseRegion( |
355 | child: const SizedBox(width: 100.0, height: 100.0), |
356 | onEnter: (PointerEnterEvent details) => enter = details, |
357 | onHover: (PointerHoverEvent details) => move = details, |
358 | onExit: (PointerExitEvent details) => exit = details, |
359 | ), |
360 | ), |
361 | ); |
362 | await tester.pump(); |
363 | expect(move, isNull); |
364 | expect(enter, isNotNull); |
365 | expect(enter!.position, equals(const Offset(400.0, 300.0))); |
366 | expect(enter!.localPosition, equals(const Offset(50.0, 50.0))); |
367 | expect(exit, isNull); |
368 | }); |
369 | |
370 | testWidgets("doesn't trigger pointer exit when widget disappears" , (WidgetTester tester) async { |
371 | PointerEnterEvent? enter; |
372 | PointerHoverEvent? move; |
373 | PointerExitEvent? exit; |
374 | await tester.pumpWidget( |
375 | Center( |
376 | child: MouseRegion( |
377 | child: const SizedBox(width: 100.0, height: 100.0), |
378 | onEnter: (PointerEnterEvent details) => enter = details, |
379 | onHover: (PointerHoverEvent details) => move = details, |
380 | onExit: (PointerExitEvent details) => exit = details, |
381 | ), |
382 | ), |
383 | ); |
384 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
385 | await gesture.addPointer(location: Offset.zero); |
386 | await gesture.moveTo(const Offset(400.0, 300.0)); |
387 | await tester.pump(); |
388 | move = null; |
389 | enter = null; |
390 | exit = null; |
391 | await tester.pumpWidget(const Center(child: SizedBox(width: 100.0, height: 100.0))); |
392 | expect(enter, isNull); |
393 | expect(move, isNull); |
394 | expect(exit, isNull); |
395 | }); |
396 | |
397 | testWidgets('triggers pointer enter when widget moves in' , (WidgetTester tester) async { |
398 | PointerEnterEvent? enter; |
399 | PointerHoverEvent? move; |
400 | PointerExitEvent? exit; |
401 | await tester.pumpWidget( |
402 | Container( |
403 | alignment: Alignment.topLeft, |
404 | child: MouseRegion( |
405 | child: const SizedBox(width: 100.0, height: 100.0), |
406 | onEnter: (PointerEnterEvent details) => enter = details, |
407 | onHover: (PointerHoverEvent details) => move = details, |
408 | onExit: (PointerExitEvent details) => exit = details, |
409 | ), |
410 | ), |
411 | ); |
412 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
413 | await gesture.addPointer(location: const Offset(401.0, 301.0)); |
414 | await tester.pump(); |
415 | expect(enter, isNull); |
416 | expect(move, isNull); |
417 | expect(exit, isNull); |
418 | await tester.pumpWidget( |
419 | Container( |
420 | alignment: Alignment.center, |
421 | child: MouseRegion( |
422 | child: const SizedBox(width: 100.0, height: 100.0), |
423 | onEnter: (PointerEnterEvent details) => enter = details, |
424 | onHover: (PointerHoverEvent details) => move = details, |
425 | onExit: (PointerExitEvent details) => exit = details, |
426 | ), |
427 | ), |
428 | ); |
429 | await tester.pump(); |
430 | expect(enter, isNotNull); |
431 | expect(enter!.position, equals(const Offset(401.0, 301.0))); |
432 | expect(enter!.localPosition, equals(const Offset(51.0, 51.0))); |
433 | expect(move, isNull); |
434 | expect(exit, isNull); |
435 | }); |
436 | |
437 | testWidgets('triggers pointer exit when widget moves out' , (WidgetTester tester) async { |
438 | PointerEnterEvent? enter; |
439 | PointerHoverEvent? move; |
440 | PointerExitEvent? exit; |
441 | await tester.pumpWidget( |
442 | Container( |
443 | alignment: Alignment.center, |
444 | child: MouseRegion( |
445 | child: const SizedBox(width: 100.0, height: 100.0), |
446 | onEnter: (PointerEnterEvent details) => enter = details, |
447 | onHover: (PointerHoverEvent details) => move = details, |
448 | onExit: (PointerExitEvent details) => exit = details, |
449 | ), |
450 | ), |
451 | ); |
452 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
453 | await gesture.addPointer(location: const Offset(400, 300)); |
454 | await tester.pump(); |
455 | enter = null; |
456 | move = null; |
457 | exit = null; |
458 | await tester.pumpWidget( |
459 | Container( |
460 | alignment: Alignment.topLeft, |
461 | child: MouseRegion( |
462 | child: const SizedBox(width: 100.0, height: 100.0), |
463 | onEnter: (PointerEnterEvent details) => enter = details, |
464 | onHover: (PointerHoverEvent details) => move = details, |
465 | onExit: (PointerExitEvent details) => exit = details, |
466 | ), |
467 | ), |
468 | ); |
469 | await tester.pump(); |
470 | expect(enter, isNull); |
471 | expect(move, isNull); |
472 | expect(exit, isNotNull); |
473 | expect(exit!.position, equals(const Offset(400, 300))); |
474 | expect(exit!.localPosition, equals(const Offset(50, 50))); |
475 | }); |
476 | |
477 | testWidgets('detects hover from touch devices' , (WidgetTester tester) async { |
478 | PointerEnterEvent? enter; |
479 | PointerHoverEvent? move; |
480 | PointerExitEvent? exit; |
481 | await tester.pumpWidget( |
482 | Center( |
483 | child: MouseRegion( |
484 | child: Container( |
485 | color: const Color.fromARGB(0xff, 0xff, 0x00, 0x00), |
486 | width: 100.0, |
487 | height: 100.0, |
488 | ), |
489 | onEnter: (PointerEnterEvent details) => enter = details, |
490 | onHover: (PointerHoverEvent details) => move = details, |
491 | onExit: (PointerExitEvent details) => exit = details, |
492 | ), |
493 | ), |
494 | ); |
495 | final TestGesture gesture = await tester.createGesture(); |
496 | await gesture.addPointer(location: Offset.zero); |
497 | await tester.pump(); |
498 | move = null; |
499 | enter = null; |
500 | exit = null; |
501 | await gesture.moveTo(const Offset(400.0, 300.0)); |
502 | expect(move, isNotNull); |
503 | expect(move!.position, equals(const Offset(400.0, 300.0))); |
504 | expect(move!.localPosition, equals(const Offset(50.0, 50.0))); |
505 | expect(enter, isNull); |
506 | expect(exit, isNull); |
507 | }); |
508 | |
509 | testWidgets('Hover works with nested listeners' , (WidgetTester tester) async { |
510 | final UniqueKey key1 = UniqueKey(); |
511 | final UniqueKey key2 = UniqueKey(); |
512 | final List<PointerEnterEvent> enter1 = <PointerEnterEvent>[]; |
513 | final List<PointerHoverEvent> move1 = <PointerHoverEvent>[]; |
514 | final List<PointerExitEvent> exit1 = <PointerExitEvent>[]; |
515 | final List<PointerEnterEvent> enter2 = <PointerEnterEvent>[]; |
516 | final List<PointerHoverEvent> move2 = <PointerHoverEvent>[]; |
517 | final List<PointerExitEvent> exit2 = <PointerExitEvent>[]; |
518 | void clearLists() { |
519 | enter1.clear(); |
520 | move1.clear(); |
521 | exit1.clear(); |
522 | enter2.clear(); |
523 | move2.clear(); |
524 | exit2.clear(); |
525 | } |
526 | |
527 | await tester.pumpWidget(Container()); |
528 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
529 | await gesture.moveTo(const Offset(400.0, 0.0)); |
530 | await tester.pump(); |
531 | await tester.pumpWidget( |
532 | Column( |
533 | mainAxisAlignment: MainAxisAlignment.center, |
534 | children: <Widget>[ |
535 | MouseRegion( |
536 | onEnter: (PointerEnterEvent details) => enter1.add(details), |
537 | onHover: (PointerHoverEvent details) => move1.add(details), |
538 | onExit: (PointerExitEvent details) => exit1.add(details), |
539 | key: key1, |
540 | child: Container( |
541 | width: 200, |
542 | height: 200, |
543 | padding: const EdgeInsets.all(50.0), |
544 | child: MouseRegion( |
545 | key: key2, |
546 | onEnter: (PointerEnterEvent details) => enter2.add(details), |
547 | onHover: (PointerHoverEvent details) => move2.add(details), |
548 | onExit: (PointerExitEvent details) => exit2.add(details), |
549 | child: Container(), |
550 | ), |
551 | ), |
552 | ), |
553 | ], |
554 | ), |
555 | ); |
556 | Offset center = tester.getCenter(find.byKey(key2)); |
557 | await gesture.moveTo(center); |
558 | await tester.pump(); |
559 | expect(move2, isNotEmpty); |
560 | expect(enter2, isNotEmpty); |
561 | expect(exit2, isEmpty); |
562 | expect(move1, isNotEmpty); |
563 | expect(move1.last.position, equals(center)); |
564 | expect(enter1, isNotEmpty); |
565 | expect(enter1.last.position, equals(center)); |
566 | expect(exit1, isEmpty); |
567 | clearLists(); |
568 | |
569 | // Now make sure that exiting the child only triggers the child exit, not |
570 | // the parent too. |
571 | center = center - const Offset(75.0, 0.0); |
572 | await gesture.moveTo(center); |
573 | await tester.pumpAndSettle(); |
574 | expect(move2, isEmpty); |
575 | expect(enter2, isEmpty); |
576 | expect(exit2, isNotEmpty); |
577 | expect(move1, isNotEmpty); |
578 | expect(move1.last.position, equals(center)); |
579 | expect(enter1, isEmpty); |
580 | expect(exit1, isEmpty); |
581 | clearLists(); |
582 | }); |
583 | |
584 | testWidgets('Hover transfers between two listeners' , (WidgetTester tester) async { |
585 | final UniqueKey key1 = UniqueKey(); |
586 | final UniqueKey key2 = UniqueKey(); |
587 | final List<PointerEnterEvent> enter1 = <PointerEnterEvent>[]; |
588 | final List<PointerHoverEvent> move1 = <PointerHoverEvent>[]; |
589 | final List<PointerExitEvent> exit1 = <PointerExitEvent>[]; |
590 | final List<PointerEnterEvent> enter2 = <PointerEnterEvent>[]; |
591 | final List<PointerHoverEvent> move2 = <PointerHoverEvent>[]; |
592 | final List<PointerExitEvent> exit2 = <PointerExitEvent>[]; |
593 | void clearLists() { |
594 | enter1.clear(); |
595 | move1.clear(); |
596 | exit1.clear(); |
597 | enter2.clear(); |
598 | move2.clear(); |
599 | exit2.clear(); |
600 | } |
601 | |
602 | await tester.pumpWidget(Container()); |
603 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
604 | await gesture.moveTo(const Offset(400.0, 0.0)); |
605 | await tester.pump(); |
606 | await tester.pumpWidget( |
607 | Column( |
608 | mainAxisAlignment: MainAxisAlignment.center, |
609 | children: <Widget>[ |
610 | MouseRegion( |
611 | key: key1, |
612 | child: const SizedBox(width: 100.0, height: 100.0), |
613 | onEnter: (PointerEnterEvent details) => enter1.add(details), |
614 | onHover: (PointerHoverEvent details) => move1.add(details), |
615 | onExit: (PointerExitEvent details) => exit1.add(details), |
616 | ), |
617 | MouseRegion( |
618 | key: key2, |
619 | child: const SizedBox(width: 100.0, height: 100.0), |
620 | onEnter: (PointerEnterEvent details) => enter2.add(details), |
621 | onHover: (PointerHoverEvent details) => move2.add(details), |
622 | onExit: (PointerExitEvent details) => exit2.add(details), |
623 | ), |
624 | ], |
625 | ), |
626 | ); |
627 | final Offset center1 = tester.getCenter(find.byKey(key1)); |
628 | final Offset center2 = tester.getCenter(find.byKey(key2)); |
629 | await gesture.moveTo(center1); |
630 | await tester.pump(); |
631 | expect(move1, isNotEmpty); |
632 | expect(move1.last.position, equals(center1)); |
633 | expect(enter1, isNotEmpty); |
634 | expect(enter1.last.position, equals(center1)); |
635 | expect(exit1, isEmpty); |
636 | expect(move2, isEmpty); |
637 | expect(enter2, isEmpty); |
638 | expect(exit2, isEmpty); |
639 | clearLists(); |
640 | await gesture.moveTo(center2); |
641 | await tester.pump(); |
642 | expect(move1, isEmpty); |
643 | expect(enter1, isEmpty); |
644 | expect(exit1, isNotEmpty); |
645 | expect(exit1.last.position, equals(center2)); |
646 | expect(move2, isNotEmpty); |
647 | expect(move2.last.position, equals(center2)); |
648 | expect(enter2, isNotEmpty); |
649 | expect(enter2.last.position, equals(center2)); |
650 | expect(exit2, isEmpty); |
651 | clearLists(); |
652 | await gesture.moveTo(const Offset(400.0, 450.0)); |
653 | await tester.pump(); |
654 | expect(move1, isEmpty); |
655 | expect(enter1, isEmpty); |
656 | expect(exit1, isEmpty); |
657 | expect(move2, isEmpty); |
658 | expect(enter2, isEmpty); |
659 | expect(exit2, isNotEmpty); |
660 | expect(exit2.last.position, equals(const Offset(400.0, 450.0))); |
661 | clearLists(); |
662 | await tester.pumpWidget(Container()); |
663 | expect(move1, isEmpty); |
664 | expect(enter1, isEmpty); |
665 | expect(exit1, isEmpty); |
666 | expect(move2, isEmpty); |
667 | expect(enter2, isEmpty); |
668 | expect(exit2, isEmpty); |
669 | }); |
670 | |
671 | testWidgets('applies mouse cursor' , (WidgetTester tester) async { |
672 | await tester.pumpWidget( |
673 | const _Scaffold( |
674 | topLeft: MouseRegion( |
675 | cursor: SystemMouseCursors.text, |
676 | child: SizedBox(width: 10, height: 10), |
677 | ), |
678 | ), |
679 | ); |
680 | |
681 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
682 | await gesture.addPointer(location: const Offset(100, 100)); |
683 | |
684 | await tester.pump(); |
685 | expect( |
686 | RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), |
687 | SystemMouseCursors.basic, |
688 | ); |
689 | |
690 | await gesture.moveTo(const Offset(5, 5)); |
691 | expect( |
692 | RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), |
693 | SystemMouseCursors.text, |
694 | ); |
695 | |
696 | await gesture.moveTo(const Offset(100, 100)); |
697 | expect( |
698 | RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), |
699 | SystemMouseCursors.basic, |
700 | ); |
701 | }); |
702 | |
703 | testWidgets('MouseRegion uses updated callbacks' , (WidgetTester tester) async { |
704 | final List<String> logs = <String>[]; |
705 | Widget hoverableContainer({ |
706 | PointerEnterEventListener? onEnter, |
707 | PointerHoverEventListener? onHover, |
708 | PointerExitEventListener? onExit, |
709 | }) { |
710 | return Container( |
711 | alignment: Alignment.topLeft, |
712 | child: MouseRegion( |
713 | onEnter: onEnter, |
714 | onHover: onHover, |
715 | onExit: onExit, |
716 | child: Container( |
717 | color: const Color.fromARGB(0xff, 0xff, 0x00, 0x00), |
718 | width: 100.0, |
719 | height: 100.0, |
720 | ), |
721 | ), |
722 | ); |
723 | } |
724 | |
725 | await tester.pumpWidget( |
726 | hoverableContainer( |
727 | onEnter: (PointerEnterEvent details) { |
728 | logs.add('enter1' ); |
729 | }, |
730 | onHover: (PointerHoverEvent details) { |
731 | logs.add('hover1' ); |
732 | }, |
733 | onExit: (PointerExitEvent details) { |
734 | logs.add('exit1' ); |
735 | }, |
736 | ), |
737 | ); |
738 | |
739 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
740 | await gesture.addPointer(location: const Offset(150.0, 150.0)); |
741 | |
742 | // Start outside, move inside, then move outside |
743 | await gesture.moveTo(const Offset(150.0, 150.0)); |
744 | await tester.pump(); |
745 | expect(logs, isEmpty); |
746 | logs.clear(); |
747 | await gesture.moveTo(const Offset(50.0, 50.0)); |
748 | await tester.pump(); |
749 | await gesture.moveTo(const Offset(150.0, 150.0)); |
750 | await tester.pump(); |
751 | expect(logs, <String>['enter1' , 'hover1' , 'exit1' ]); |
752 | logs.clear(); |
753 | |
754 | // Same tests but with updated callbacks |
755 | await tester.pumpWidget( |
756 | hoverableContainer( |
757 | onEnter: (PointerEnterEvent details) => logs.add('enter2' ), |
758 | onHover: (PointerHoverEvent details) => logs.add('hover2' ), |
759 | onExit: (PointerExitEvent details) => logs.add('exit2' ), |
760 | ), |
761 | ); |
762 | await gesture.moveTo(const Offset(150.0, 150.0)); |
763 | await tester.pump(); |
764 | await gesture.moveTo(const Offset(50.0, 50.0)); |
765 | await tester.pump(); |
766 | await gesture.moveTo(const Offset(150.0, 150.0)); |
767 | await tester.pump(); |
768 | expect(logs, <String>['enter2' , 'hover2' , 'exit2' ]); |
769 | }); |
770 | |
771 | testWidgets('needsCompositing set when parent class needsCompositing is set' , ( |
772 | WidgetTester tester, |
773 | ) async { |
774 | await tester.pumpWidget( |
775 | MouseRegion( |
776 | onEnter: (PointerEnterEvent _) {}, |
777 | child: const RepaintBoundary(child: Placeholder()), |
778 | ), |
779 | ); |
780 | |
781 | RenderMouseRegion listener = tester.renderObject(find.byType(MouseRegion).first); |
782 | expect(listener.needsCompositing, isTrue); |
783 | |
784 | await tester.pumpWidget( |
785 | MouseRegion(onEnter: (PointerEnterEvent _) {}, child: const Placeholder()), |
786 | ); |
787 | |
788 | listener = tester.renderObject(find.byType(MouseRegion).first); |
789 | expect(listener.needsCompositing, isFalse); |
790 | }); |
791 | |
792 | testWidgets('works with transform' , (WidgetTester tester) async { |
793 | // Regression test for https://github.com/flutter/flutter/issues/31986. |
794 | final Key key = UniqueKey(); |
795 | const double scaleFactor = 2.0; |
796 | const double localWidth = 150.0; |
797 | const double localHeight = 100.0; |
798 | final List<PointerEvent> events = <PointerEvent>[]; |
799 | |
800 | await tester.pumpWidget( |
801 | MaterialApp( |
802 | home: Center( |
803 | child: Transform.scale( |
804 | scale: scaleFactor, |
805 | child: MouseRegion( |
806 | onEnter: (PointerEnterEvent event) { |
807 | events.add(event); |
808 | }, |
809 | onHover: (PointerHoverEvent event) { |
810 | events.add(event); |
811 | }, |
812 | onExit: (PointerExitEvent event) { |
813 | events.add(event); |
814 | }, |
815 | child: Container( |
816 | key: key, |
817 | color: Colors.blue, |
818 | height: localHeight, |
819 | width: localWidth, |
820 | child: const Text('Hi' ), |
821 | ), |
822 | ), |
823 | ), |
824 | ), |
825 | ), |
826 | ); |
827 | |
828 | final Offset topLeft = tester.getTopLeft(find.byKey(key)); |
829 | final Offset topRight = tester.getTopRight(find.byKey(key)); |
830 | final Offset bottomLeft = tester.getBottomLeft(find.byKey(key)); |
831 | expect(topRight.dx - topLeft.dx, scaleFactor * localWidth); |
832 | expect(bottomLeft.dy - topLeft.dy, scaleFactor * localHeight); |
833 | |
834 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
835 | await gesture.addPointer(); |
836 | await gesture.moveTo(topLeft - const Offset(1, 1)); |
837 | await tester.pump(); |
838 | expect(events, isEmpty); |
839 | |
840 | await gesture.moveTo(topLeft + const Offset(1, 1)); |
841 | await tester.pump(); |
842 | expect(events, hasLength(2)); |
843 | expect(events.first, isA<PointerEnterEvent>()); |
844 | expect(events.last, isA<PointerHoverEvent>()); |
845 | events.clear(); |
846 | |
847 | await gesture.moveTo(bottomLeft + const Offset(1, -1)); |
848 | await tester.pump(); |
849 | expect(events.single, isA<PointerHoverEvent>()); |
850 | expect(events.single.delta, const Offset(0.0, scaleFactor * localHeight - 2)); |
851 | events.clear(); |
852 | |
853 | await gesture.moveTo(bottomLeft + const Offset(1, 1)); |
854 | await tester.pump(); |
855 | expect(events.single, isA<PointerExitEvent>()); |
856 | events.clear(); |
857 | }); |
858 | |
859 | testWidgets('needsCompositing is always false' , (WidgetTester tester) async { |
860 | // Pretend that we have a mouse connected. |
861 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
862 | await gesture.addPointer(); |
863 | |
864 | await tester.pumpWidget(Transform.scale(scale: 2.0, child: const MouseRegion(opaque: false))); |
865 | final RenderMouseRegion mouseRegion = tester.renderObject(find.byType(MouseRegion)); |
866 | expect(mouseRegion.needsCompositing, isFalse); |
867 | // No TransformLayer for `Transform.scale` is added because composting is |
868 | // not required and therefore the transform is executed on the canvas |
869 | // directly. (One TransformLayer is always present for the root |
870 | // transform.) |
871 | expect(tester.layers.whereType<TransformLayer>(), hasLength(1)); |
872 | |
873 | // Test that needsCompositing stays false with callback change |
874 | await tester.pumpWidget( |
875 | Transform.scale( |
876 | scale: 2.0, |
877 | child: MouseRegion(opaque: false, onHover: (PointerHoverEvent _) {}), |
878 | ), |
879 | ); |
880 | expect(mouseRegion.needsCompositing, isFalse); |
881 | // If compositing was required, a dedicated TransformLayer for |
882 | // `Transform.scale` would be added. |
883 | expect(tester.layers.whereType<TransformLayer>(), hasLength(1)); |
884 | }); |
885 | |
886 | testWidgets("Callbacks aren't called during build" , (WidgetTester tester) async { |
887 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
888 | await gesture.addPointer(location: Offset.zero); |
889 | |
890 | int numEntrances = 0; |
891 | int numExits = 0; |
892 | |
893 | await tester.pumpWidget( |
894 | Center( |
895 | child: HoverFeedback( |
896 | onEnter: () { |
897 | numEntrances += 1; |
898 | }, |
899 | onExit: () { |
900 | numExits += 1; |
901 | }, |
902 | ), |
903 | ), |
904 | ); |
905 | |
906 | await gesture.moveTo(tester.getCenter(find.byType(Text))); |
907 | await tester.pumpAndSettle(); |
908 | expect(numEntrances, equals(1)); |
909 | expect(numExits, equals(0)); |
910 | expect(find.text('HOVERING' ), findsOneWidget); |
911 | |
912 | await tester.pumpWidget(Container()); |
913 | await tester.pump(); |
914 | expect(numEntrances, equals(1)); |
915 | expect(numExits, equals(0)); |
916 | |
917 | await tester.pumpWidget( |
918 | Center( |
919 | child: HoverFeedback( |
920 | onEnter: () { |
921 | numEntrances += 1; |
922 | }, |
923 | onExit: () { |
924 | numExits += 1; |
925 | }, |
926 | ), |
927 | ), |
928 | ); |
929 | await tester.pump(); |
930 | expect(numEntrances, equals(2)); |
931 | expect(numExits, equals(0)); |
932 | }); |
933 | |
934 | testWidgets("MouseRegion activate/deactivate don't duplicate annotations" , ( |
935 | WidgetTester tester, |
936 | ) async { |
937 | final GlobalKey feedbackKey = GlobalKey(); |
938 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
939 | await gesture.addPointer(); |
940 | |
941 | int numEntrances = 0; |
942 | int numExits = 0; |
943 | |
944 | await tester.pumpWidget( |
945 | Center( |
946 | child: HoverFeedback( |
947 | key: feedbackKey, |
948 | onEnter: () { |
949 | numEntrances += 1; |
950 | }, |
951 | onExit: () { |
952 | numExits += 1; |
953 | }, |
954 | ), |
955 | ), |
956 | ); |
957 | |
958 | await gesture.moveTo(tester.getCenter(find.byType(Text))); |
959 | await tester.pumpAndSettle(); |
960 | expect(numEntrances, equals(1)); |
961 | expect(numExits, equals(0)); |
962 | expect(find.text('HOVERING' ), findsOneWidget); |
963 | |
964 | await tester.pumpWidget( |
965 | Center( |
966 | child: HoverFeedback( |
967 | key: feedbackKey, |
968 | onEnter: () { |
969 | numEntrances += 1; |
970 | }, |
971 | onExit: () { |
972 | numExits += 1; |
973 | }, |
974 | ), |
975 | ), |
976 | ); |
977 | await tester.pump(); |
978 | expect(numEntrances, equals(1)); |
979 | expect(numExits, equals(0)); |
980 | await tester.pumpWidget(Container()); |
981 | await tester.pump(); |
982 | expect(numEntrances, equals(1)); |
983 | expect(numExits, equals(0)); |
984 | }); |
985 | |
986 | testWidgets('Exit event when unplugging mouse should have a position' , ( |
987 | WidgetTester tester, |
988 | ) async { |
989 | final List<PointerEnterEvent> enter = <PointerEnterEvent>[]; |
990 | final List<PointerHoverEvent> hover = <PointerHoverEvent>[]; |
991 | final List<PointerExitEvent> exit = <PointerExitEvent>[]; |
992 | |
993 | await tester.pumpWidget( |
994 | Center( |
995 | child: MouseRegion( |
996 | onEnter: (PointerEnterEvent e) => enter.add(e), |
997 | onHover: (PointerHoverEvent e) => hover.add(e), |
998 | onExit: (PointerExitEvent e) => exit.add(e), |
999 | child: const SizedBox(height: 100.0, width: 100.0), |
1000 | ), |
1001 | ), |
1002 | ); |
1003 | |
1004 | // Plug-in a mouse and move it to the center of the container. |
1005 | TestGesture? gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
1006 | await gesture.addPointer(location: Offset.zero); |
1007 | addTearDown(() => gesture?.removePointer()); |
1008 | await tester.pumpAndSettle(); |
1009 | await gesture.moveTo(tester.getCenter(find.byType(SizedBox))); |
1010 | |
1011 | expect(enter.length, 1); |
1012 | expect(enter.single.position, const Offset(400.0, 300.0)); |
1013 | expect(hover.length, 1); |
1014 | expect(hover.single.position, const Offset(400.0, 300.0)); |
1015 | expect(exit.length, 0); |
1016 | |
1017 | enter.clear(); |
1018 | hover.clear(); |
1019 | exit.clear(); |
1020 | |
1021 | // Unplug the mouse. |
1022 | await gesture.removePointer(); |
1023 | gesture = null; |
1024 | await tester.pumpAndSettle(); |
1025 | |
1026 | expect(enter.length, 0); |
1027 | expect(hover.length, 0); |
1028 | expect(exit.length, 1); |
1029 | expect(exit.single.position, const Offset(400.0, 300.0)); |
1030 | expect(exit.single.delta, Offset.zero); |
1031 | }); |
1032 | |
1033 | testWidgets('detects pointer enter with closure arguments' , (WidgetTester tester) async { |
1034 | await tester.pumpWidget(const _HoverClientWithClosures()); |
1035 | expect(find.text('not hovering' ), findsOneWidget); |
1036 | |
1037 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
1038 | await gesture.addPointer(); |
1039 | // Move to a position out of MouseRegion |
1040 | await gesture.moveTo(tester.getBottomRight(find.byType(MouseRegion)) + const Offset(10, -10)); |
1041 | await tester.pumpAndSettle(); |
1042 | expect(find.text('not hovering' ), findsOneWidget); |
1043 | |
1044 | // Move into MouseRegion |
1045 | await gesture.moveBy(const Offset(-20, 0)); |
1046 | await tester.pumpAndSettle(); |
1047 | expect(find.text('HOVERING' ), findsOneWidget); |
1048 | }); |
1049 | |
1050 | testWidgets('MouseRegion paints child once and only once when MouseRegion is inactive' , ( |
1051 | WidgetTester tester, |
1052 | ) async { |
1053 | int paintCount = 0; |
1054 | await tester.pumpWidget( |
1055 | Directionality( |
1056 | textDirection: TextDirection.ltr, |
1057 | child: MouseRegion( |
1058 | onEnter: (PointerEnterEvent e) {}, |
1059 | child: CustomPaint( |
1060 | painter: _DelegatedPainter( |
1061 | onPaint: () { |
1062 | paintCount += 1; |
1063 | }, |
1064 | ), |
1065 | child: const Text('123' ), |
1066 | ), |
1067 | ), |
1068 | ), |
1069 | ); |
1070 | |
1071 | expect(paintCount, 1); |
1072 | }); |
1073 | |
1074 | testWidgets('MouseRegion paints child once and only once when MouseRegion is active' , ( |
1075 | WidgetTester tester, |
1076 | ) async { |
1077 | int paintCount = 0; |
1078 | |
1079 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
1080 | await gesture.addPointer(); |
1081 | |
1082 | await tester.pumpWidget( |
1083 | Directionality( |
1084 | textDirection: TextDirection.ltr, |
1085 | child: MouseRegion( |
1086 | onEnter: (PointerEnterEvent e) {}, |
1087 | child: CustomPaint( |
1088 | painter: _DelegatedPainter( |
1089 | onPaint: () { |
1090 | paintCount += 1; |
1091 | }, |
1092 | ), |
1093 | child: const Text('123' ), |
1094 | ), |
1095 | ), |
1096 | ), |
1097 | ); |
1098 | |
1099 | expect(paintCount, 1); |
1100 | }); |
1101 | |
1102 | testWidgets('A MouseRegion mounted under the pointer should take effect in the next postframe' , ( |
1103 | WidgetTester tester, |
1104 | ) async { |
1105 | bool hovered = false; |
1106 | |
1107 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
1108 | await gesture.addPointer(location: const Offset(5, 5)); |
1109 | |
1110 | await tester.pumpWidget( |
1111 | StatefulBuilder( |
1112 | builder: (BuildContext context, StateSetter setState) { |
1113 | return _ColumnContainer( |
1114 | children: <Widget>[Text(hovered ? 'hover outer' : 'unhover outer' )], |
1115 | ); |
1116 | }, |
1117 | ), |
1118 | ); |
1119 | |
1120 | expect(find.text('unhover outer' ), findsOneWidget); |
1121 | |
1122 | await tester.pumpWidget( |
1123 | StatefulBuilder( |
1124 | builder: (BuildContext context, StateSetter setState) { |
1125 | return _ColumnContainer( |
1126 | children: <Widget>[ |
1127 | HoverClient( |
1128 | onHover: (bool value) { |
1129 | setState(() { |
1130 | hovered = value; |
1131 | }); |
1132 | }, |
1133 | child: Text(hovered ? 'hover inner' : 'unhover inner' ), |
1134 | ), |
1135 | Text(hovered ? 'hover outer' : 'unhover outer' ), |
1136 | ], |
1137 | ); |
1138 | }, |
1139 | ), |
1140 | ); |
1141 | |
1142 | expect(find.text('unhover outer' ), findsOneWidget); |
1143 | expect(find.text('unhover inner' ), findsOneWidget); |
1144 | |
1145 | await tester.pump(); |
1146 | |
1147 | expect(find.text('hover outer' ), findsOneWidget); |
1148 | expect(find.text('hover inner' ), findsOneWidget); |
1149 | expect(tester.binding.hasScheduledFrame, isFalse); |
1150 | }); |
1151 | |
1152 | testWidgets('A MouseRegion unmounted under the pointer should not trigger state change' , ( |
1153 | WidgetTester tester, |
1154 | ) async { |
1155 | bool hovered = true; |
1156 | |
1157 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
1158 | await gesture.addPointer(location: const Offset(5, 5)); |
1159 | |
1160 | await tester.pumpWidget( |
1161 | StatefulBuilder( |
1162 | builder: (BuildContext context, StateSetter setState) { |
1163 | return _ColumnContainer( |
1164 | children: <Widget>[ |
1165 | HoverClient( |
1166 | onHover: (bool value) { |
1167 | setState(() { |
1168 | hovered = value; |
1169 | }); |
1170 | }, |
1171 | child: Text(hovered ? 'hover inner' : 'unhover inner' ), |
1172 | ), |
1173 | Text(hovered ? 'hover outer' : 'unhover outer' ), |
1174 | ], |
1175 | ); |
1176 | }, |
1177 | ), |
1178 | ); |
1179 | |
1180 | expect(find.text('hover outer' ), findsOneWidget); |
1181 | expect(find.text('hover inner' ), findsOneWidget); |
1182 | expect(tester.binding.hasScheduledFrame, isTrue); |
1183 | |
1184 | await tester.pump(); |
1185 | expect(find.text('hover outer' ), findsOneWidget); |
1186 | expect(find.text('hover inner' ), findsOneWidget); |
1187 | expect(tester.binding.hasScheduledFrame, isFalse); |
1188 | |
1189 | await tester.pumpWidget( |
1190 | StatefulBuilder( |
1191 | builder: (BuildContext context, StateSetter setState) { |
1192 | return _ColumnContainer( |
1193 | children: <Widget>[Text(hovered ? 'hover outer' : 'unhover outer' )], |
1194 | ); |
1195 | }, |
1196 | ), |
1197 | ); |
1198 | |
1199 | expect(find.text('hover outer' ), findsOneWidget); |
1200 | expect(tester.binding.hasScheduledFrame, isFalse); |
1201 | }); |
1202 | |
1203 | testWidgets('A MouseRegion moved into the mouse should take effect in the next postframe' , ( |
1204 | WidgetTester tester, |
1205 | ) async { |
1206 | bool hovered = false; |
1207 | final List<bool> logHovered = <bool>[]; |
1208 | bool moved = false; |
1209 | late StateSetter mySetState; |
1210 | |
1211 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
1212 | await gesture.addPointer(location: const Offset(5, 5)); |
1213 | |
1214 | await tester.pumpWidget( |
1215 | StatefulBuilder( |
1216 | builder: (BuildContext context, StateSetter setState) { |
1217 | mySetState = setState; |
1218 | return _ColumnContainer( |
1219 | children: <Widget>[ |
1220 | Container( |
1221 | height: 100, |
1222 | width: 10, |
1223 | alignment: moved ? Alignment.topLeft : Alignment.bottomLeft, |
1224 | child: SizedBox( |
1225 | height: 10, |
1226 | width: 10, |
1227 | child: HoverClient( |
1228 | onHover: (bool value) { |
1229 | setState(() { |
1230 | hovered = value; |
1231 | }); |
1232 | logHovered.add(value); |
1233 | }, |
1234 | child: Text(hovered ? 'hover inner' : 'unhover inner' ), |
1235 | ), |
1236 | ), |
1237 | ), |
1238 | Text(hovered ? 'hover outer' : 'unhover outer' ), |
1239 | ], |
1240 | ); |
1241 | }, |
1242 | ), |
1243 | ); |
1244 | |
1245 | expect(find.text('unhover inner' ), findsOneWidget); |
1246 | expect(find.text('unhover outer' ), findsOneWidget); |
1247 | expect(logHovered, isEmpty); |
1248 | expect(tester.binding.hasScheduledFrame, isFalse); |
1249 | |
1250 | mySetState(() { |
1251 | moved = true; |
1252 | }); |
1253 | // The first frame is for the widget movement to take effect. |
1254 | await tester.pump(); |
1255 | expect(find.text('unhover inner' ), findsOneWidget); |
1256 | expect(find.text('unhover outer' ), findsOneWidget); |
1257 | expect(logHovered, <bool>[true]); |
1258 | logHovered.clear(); |
1259 | |
1260 | // The second frame is for the mouse hover to take effect. |
1261 | await tester.pump(); |
1262 | expect(find.text('hover inner' ), findsOneWidget); |
1263 | expect(find.text('hover outer' ), findsOneWidget); |
1264 | expect(logHovered, isEmpty); |
1265 | expect(tester.binding.hasScheduledFrame, isFalse); |
1266 | }); |
1267 | |
1268 | group('MouseRegion respects opacity:' , () { |
1269 | // A widget that contains 3 MouseRegions: |
1270 | // y |
1271 | // —————————————————————— 0 |
1272 | // | ——————————— A | 20 |
1273 | // | | B | | |
1274 | // | | ——————————— | 50 |
1275 | // | | | C | | |
1276 | // | ——————| | | 100 |
1277 | // | | | | |
1278 | // | ——————————— | 130 |
1279 | // —————————————————————— 150 |
1280 | // x 0 20 50 100 130 150 |
1281 | Widget tripleRegions({bool? opaqueC, required void Function(String) addLog}) { |
1282 | // Same as MouseRegion, but when opaque is null, use the default value. |
1283 | Widget mouseRegionWithOptionalOpaque({ |
1284 | void Function(PointerEnterEvent e)? onEnter, |
1285 | void Function(PointerHoverEvent e)? onHover, |
1286 | void Function(PointerExitEvent e)? onExit, |
1287 | Widget? child, |
1288 | bool? opaque, |
1289 | }) { |
1290 | if (opaque == null) { |
1291 | return MouseRegion(onEnter: onEnter, onHover: onHover, onExit: onExit, child: child); |
1292 | } |
1293 | return MouseRegion( |
1294 | onEnter: onEnter, |
1295 | onHover: onHover, |
1296 | onExit: onExit, |
1297 | opaque: opaque, |
1298 | child: child, |
1299 | ); |
1300 | } |
1301 | |
1302 | return Directionality( |
1303 | textDirection: TextDirection.ltr, |
1304 | child: Align( |
1305 | alignment: Alignment.topLeft, |
1306 | child: MouseRegion( |
1307 | onEnter: (PointerEnterEvent e) { |
1308 | addLog('enterA' ); |
1309 | }, |
1310 | onHover: (PointerHoverEvent e) { |
1311 | addLog('hoverA' ); |
1312 | }, |
1313 | onExit: (PointerExitEvent e) { |
1314 | addLog('exitA' ); |
1315 | }, |
1316 | child: SizedBox( |
1317 | width: 150, |
1318 | height: 150, |
1319 | child: Stack( |
1320 | children: <Widget>[ |
1321 | Positioned( |
1322 | left: 20, |
1323 | top: 20, |
1324 | width: 80, |
1325 | height: 80, |
1326 | child: MouseRegion( |
1327 | onEnter: (PointerEnterEvent e) { |
1328 | addLog('enterB' ); |
1329 | }, |
1330 | onHover: (PointerHoverEvent e) { |
1331 | addLog('hoverB' ); |
1332 | }, |
1333 | onExit: (PointerExitEvent e) { |
1334 | addLog('exitB' ); |
1335 | }, |
1336 | ), |
1337 | ), |
1338 | Positioned( |
1339 | left: 50, |
1340 | top: 50, |
1341 | width: 80, |
1342 | height: 80, |
1343 | child: mouseRegionWithOptionalOpaque( |
1344 | opaque: opaqueC, |
1345 | onEnter: (PointerEnterEvent e) { |
1346 | addLog('enterC' ); |
1347 | }, |
1348 | onHover: (PointerHoverEvent e) { |
1349 | addLog('hoverC' ); |
1350 | }, |
1351 | onExit: (PointerExitEvent e) { |
1352 | addLog('exitC' ); |
1353 | }, |
1354 | ), |
1355 | ), |
1356 | ], |
1357 | ), |
1358 | ), |
1359 | ), |
1360 | ), |
1361 | ); |
1362 | } |
1363 | |
1364 | testWidgets('a transparent one should allow MouseRegions behind it to receive pointers' , ( |
1365 | WidgetTester tester, |
1366 | ) async { |
1367 | final List<String> logs = <String>[]; |
1368 | await tester.pumpWidget(tripleRegions(opaqueC: false, addLog: (String log) => logs.add(log))); |
1369 | |
1370 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
1371 | await gesture.addPointer(); |
1372 | await tester.pumpAndSettle(); |
1373 | |
1374 | // Move to the overlapping area. |
1375 | await gesture.moveTo(const Offset(75, 75)); |
1376 | await tester.pumpAndSettle(); |
1377 | expect(logs, <String>['enterA' , 'enterB' , 'enterC' , 'hoverC' , 'hoverB' , 'hoverA' ]); |
1378 | logs.clear(); |
1379 | |
1380 | // Move to the B only area. |
1381 | await gesture.moveTo(const Offset(25, 75)); |
1382 | await tester.pumpAndSettle(); |
1383 | expect(logs, <String>['exitC' , 'hoverB' , 'hoverA' ]); |
1384 | logs.clear(); |
1385 | |
1386 | // Move back to the overlapping area. |
1387 | await gesture.moveTo(const Offset(75, 75)); |
1388 | await tester.pumpAndSettle(); |
1389 | expect(logs, <String>['enterC' , 'hoverC' , 'hoverB' , 'hoverA' ]); |
1390 | logs.clear(); |
1391 | |
1392 | // Move to the C only area. |
1393 | await gesture.moveTo(const Offset(125, 75)); |
1394 | await tester.pumpAndSettle(); |
1395 | expect(logs, <String>['exitB' , 'hoverC' , 'hoverA' ]); |
1396 | logs.clear(); |
1397 | |
1398 | // Move back to the overlapping area. |
1399 | await gesture.moveTo(const Offset(75, 75)); |
1400 | await tester.pumpAndSettle(); |
1401 | expect(logs, <String>['enterB' , 'hoverC' , 'hoverB' , 'hoverA' ]); |
1402 | logs.clear(); |
1403 | |
1404 | // Move out. |
1405 | await gesture.moveTo(const Offset(160, 160)); |
1406 | await tester.pumpAndSettle(); |
1407 | expect(logs, <String>['exitC' , 'exitB' , 'exitA' ]); |
1408 | }); |
1409 | |
1410 | testWidgets('an opaque one should prevent MouseRegions behind it receiving pointers' , ( |
1411 | WidgetTester tester, |
1412 | ) async { |
1413 | final List<String> logs = <String>[]; |
1414 | await tester.pumpWidget(tripleRegions(opaqueC: true, addLog: (String log) => logs.add(log))); |
1415 | |
1416 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
1417 | await gesture.addPointer(); |
1418 | await tester.pumpAndSettle(); |
1419 | |
1420 | // Move to the overlapping area. |
1421 | await gesture.moveTo(const Offset(75, 75)); |
1422 | await tester.pumpAndSettle(); |
1423 | expect(logs, <String>['enterA' , 'enterC' , 'hoverC' , 'hoverA' ]); |
1424 | logs.clear(); |
1425 | |
1426 | // Move to the B only area. |
1427 | await gesture.moveTo(const Offset(25, 75)); |
1428 | await tester.pumpAndSettle(); |
1429 | expect(logs, <String>['exitC' , 'enterB' , 'hoverB' , 'hoverA' ]); |
1430 | logs.clear(); |
1431 | |
1432 | // Move back to the overlapping area. |
1433 | await gesture.moveTo(const Offset(75, 75)); |
1434 | await tester.pumpAndSettle(); |
1435 | expect(logs, <String>['exitB' , 'enterC' , 'hoverC' , 'hoverA' ]); |
1436 | logs.clear(); |
1437 | |
1438 | // Move to the C only area. |
1439 | await gesture.moveTo(const Offset(125, 75)); |
1440 | await tester.pumpAndSettle(); |
1441 | expect(logs, <String>['hoverC' , 'hoverA' ]); |
1442 | logs.clear(); |
1443 | |
1444 | // Move back to the overlapping area. |
1445 | await gesture.moveTo(const Offset(75, 75)); |
1446 | await tester.pumpAndSettle(); |
1447 | expect(logs, <String>['hoverC' , 'hoverA' ]); |
1448 | logs.clear(); |
1449 | |
1450 | // Move out. |
1451 | await gesture.moveTo(const Offset(160, 160)); |
1452 | await tester.pumpAndSettle(); |
1453 | expect(logs, <String>['exitC' , 'exitA' ]); |
1454 | }); |
1455 | |
1456 | testWidgets('opaque should default to true' , (WidgetTester tester) async { |
1457 | final List<String> logs = <String>[]; |
1458 | await tester.pumpWidget(tripleRegions(addLog: (String log) => logs.add(log))); |
1459 | |
1460 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
1461 | await gesture.addPointer(); |
1462 | await tester.pumpAndSettle(); |
1463 | |
1464 | // Move to the overlapping area. |
1465 | await gesture.moveTo(const Offset(75, 75)); |
1466 | await tester.pumpAndSettle(); |
1467 | expect(logs, <String>['enterA' , 'enterC' , 'hoverC' , 'hoverA' ]); |
1468 | logs.clear(); |
1469 | |
1470 | // Move out. |
1471 | await gesture.moveTo(const Offset(160, 160)); |
1472 | await tester.pumpAndSettle(); |
1473 | expect(logs, <String>['exitC' , 'exitA' ]); |
1474 | }); |
1475 | }); |
1476 | |
1477 | testWidgets('an empty opaque MouseRegion is effective' , (WidgetTester tester) async { |
1478 | bool bottomRegionIsHovered = false; |
1479 | await tester.pumpWidget( |
1480 | Directionality( |
1481 | textDirection: TextDirection.ltr, |
1482 | child: Stack( |
1483 | children: <Widget>[ |
1484 | Align( |
1485 | alignment: Alignment.topLeft, |
1486 | child: MouseRegion( |
1487 | onEnter: (_) { |
1488 | bottomRegionIsHovered = true; |
1489 | }, |
1490 | onHover: (_) { |
1491 | bottomRegionIsHovered = true; |
1492 | }, |
1493 | onExit: (_) { |
1494 | bottomRegionIsHovered = true; |
1495 | }, |
1496 | child: const SizedBox(width: 10, height: 10), |
1497 | ), |
1498 | ), |
1499 | const MouseRegion(), |
1500 | ], |
1501 | ), |
1502 | ), |
1503 | ); |
1504 | |
1505 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
1506 | await gesture.addPointer(location: const Offset(20, 20)); |
1507 | |
1508 | await gesture.moveTo(const Offset(5, 5)); |
1509 | await tester.pump(); |
1510 | await gesture.moveTo(const Offset(20, 20)); |
1511 | await tester.pump(); |
1512 | expect(bottomRegionIsHovered, isFalse); |
1513 | }); |
1514 | |
1515 | testWidgets("Changing MouseRegion's callbacks is effective and doesn't repaint" , ( |
1516 | WidgetTester tester, |
1517 | ) async { |
1518 | final List<String> logs = <String>[]; |
1519 | const Key key = ValueKey<int>(1); |
1520 | |
1521 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
1522 | await gesture.addPointer(location: const Offset(20, 20)); |
1523 | |
1524 | await tester.pumpWidget( |
1525 | _Scaffold( |
1526 | topLeft: SizedBox( |
1527 | height: 10, |
1528 | width: 10, |
1529 | child: MouseRegion( |
1530 | onEnter: (_) { |
1531 | logs.add('enter1' ); |
1532 | }, |
1533 | onHover: (_) { |
1534 | logs.add('hover1' ); |
1535 | }, |
1536 | onExit: (_) { |
1537 | logs.add('exit1' ); |
1538 | }, |
1539 | child: CustomPaint( |
1540 | painter: _DelegatedPainter( |
1541 | onPaint: () { |
1542 | logs.add('paint' ); |
1543 | }, |
1544 | key: key, |
1545 | ), |
1546 | ), |
1547 | ), |
1548 | ), |
1549 | ), |
1550 | ); |
1551 | expect(logs, <String>['paint' ]); |
1552 | logs.clear(); |
1553 | |
1554 | await gesture.moveTo(const Offset(5, 5)); |
1555 | expect(logs, <String>['enter1' , 'hover1' ]); |
1556 | logs.clear(); |
1557 | |
1558 | await tester.pumpWidget( |
1559 | _Scaffold( |
1560 | topLeft: SizedBox( |
1561 | height: 10, |
1562 | width: 10, |
1563 | child: MouseRegion( |
1564 | onEnter: (_) { |
1565 | logs.add('enter2' ); |
1566 | }, |
1567 | onHover: (_) { |
1568 | logs.add('hover2' ); |
1569 | }, |
1570 | onExit: (_) { |
1571 | logs.add('exit2' ); |
1572 | }, |
1573 | child: CustomPaint( |
1574 | painter: _DelegatedPainter( |
1575 | onPaint: () { |
1576 | logs.add('paint' ); |
1577 | }, |
1578 | key: key, |
1579 | ), |
1580 | ), |
1581 | ), |
1582 | ), |
1583 | ), |
1584 | ); |
1585 | expect(logs, isEmpty); |
1586 | |
1587 | await gesture.moveTo(const Offset(6, 6)); |
1588 | expect(logs, <String>['hover2' ]); |
1589 | logs.clear(); |
1590 | |
1591 | // Compare: It repaints if the MouseRegion is deactivated. |
1592 | await tester.pumpWidget( |
1593 | _Scaffold( |
1594 | topLeft: SizedBox( |
1595 | height: 10, |
1596 | width: 10, |
1597 | child: MouseRegion( |
1598 | opaque: false, |
1599 | child: CustomPaint( |
1600 | painter: _DelegatedPainter( |
1601 | onPaint: () { |
1602 | logs.add('paint' ); |
1603 | }, |
1604 | key: key, |
1605 | ), |
1606 | ), |
1607 | ), |
1608 | ), |
1609 | ), |
1610 | ); |
1611 | expect(logs, <String>['paint' ]); |
1612 | }); |
1613 | |
1614 | testWidgets('Changing MouseRegion.opaque is effective and repaints' , (WidgetTester tester) async { |
1615 | final List<String> logs = <String>[]; |
1616 | |
1617 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
1618 | await gesture.addPointer(location: const Offset(5, 5)); |
1619 | |
1620 | void handleHover(PointerHoverEvent _) {} |
1621 | void handlePaintChild() { |
1622 | logs.add('paint' ); |
1623 | } |
1624 | |
1625 | await tester.pumpWidget( |
1626 | _Scaffold( |
1627 | topLeft: SizedBox( |
1628 | height: 10, |
1629 | width: 10, |
1630 | child: MouseRegion( |
1631 | onHover: handleHover, |
1632 | child: CustomPaint(painter: _DelegatedPainter(onPaint: handlePaintChild)), |
1633 | ), |
1634 | ), |
1635 | background: MouseRegion( |
1636 | onEnter: (_) { |
1637 | logs.add('hover-enter' ); |
1638 | }, |
1639 | ), |
1640 | ), |
1641 | ); |
1642 | expect(logs, <String>['paint' ]); |
1643 | logs.clear(); |
1644 | |
1645 | expect(logs, isEmpty); |
1646 | logs.clear(); |
1647 | |
1648 | await tester.pumpWidget( |
1649 | _Scaffold( |
1650 | topLeft: SizedBox( |
1651 | height: 10, |
1652 | width: 10, |
1653 | child: MouseRegion( |
1654 | opaque: false, |
1655 | // Dummy callback so that MouseRegion stays affective after opaque |
1656 | // turns false. |
1657 | onHover: handleHover, |
1658 | child: CustomPaint(painter: _DelegatedPainter(onPaint: handlePaintChild)), |
1659 | ), |
1660 | ), |
1661 | background: MouseRegion( |
1662 | onEnter: (_) { |
1663 | logs.add('hover-enter' ); |
1664 | }, |
1665 | ), |
1666 | ), |
1667 | ); |
1668 | |
1669 | expect(logs, <String>['paint' , 'hover-enter' ]); |
1670 | }); |
1671 | |
1672 | testWidgets('Changing MouseRegion.cursor is effective and repaints' , (WidgetTester tester) async { |
1673 | final List<String> logPaints = <String>[]; |
1674 | final List<String> logEnters = <String>[]; |
1675 | |
1676 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
1677 | await gesture.addPointer(location: const Offset(100, 100)); |
1678 | |
1679 | void onPaintChild() { |
1680 | logPaints.add('paint' ); |
1681 | } |
1682 | |
1683 | await tester.pumpWidget( |
1684 | _Scaffold( |
1685 | topLeft: SizedBox( |
1686 | height: 10, |
1687 | width: 10, |
1688 | child: MouseRegion( |
1689 | cursor: SystemMouseCursors.forbidden, |
1690 | onEnter: (_) { |
1691 | logEnters.add('enter' ); |
1692 | }, |
1693 | child: CustomPaint(painter: _DelegatedPainter(onPaint: onPaintChild)), |
1694 | ), |
1695 | ), |
1696 | ), |
1697 | ); |
1698 | await gesture.moveTo(const Offset(5, 5)); |
1699 | |
1700 | expect(logPaints, <String>['paint' ]); |
1701 | expect( |
1702 | RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), |
1703 | SystemMouseCursors.forbidden, |
1704 | ); |
1705 | expect(logEnters, <String>['enter' ]); |
1706 | logPaints.clear(); |
1707 | logEnters.clear(); |
1708 | |
1709 | await tester.pumpWidget( |
1710 | _Scaffold( |
1711 | topLeft: SizedBox( |
1712 | height: 10, |
1713 | width: 10, |
1714 | child: MouseRegion( |
1715 | cursor: SystemMouseCursors.text, |
1716 | onEnter: (_) { |
1717 | logEnters.add('enter' ); |
1718 | }, |
1719 | child: CustomPaint(painter: _DelegatedPainter(onPaint: onPaintChild)), |
1720 | ), |
1721 | ), |
1722 | ), |
1723 | ); |
1724 | |
1725 | expect(logPaints, <String>['paint' ]); |
1726 | expect( |
1727 | RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), |
1728 | SystemMouseCursors.text, |
1729 | ); |
1730 | expect(logEnters, isEmpty); |
1731 | logPaints.clear(); |
1732 | logEnters.clear(); |
1733 | }); |
1734 | |
1735 | testWidgets('Changing whether MouseRegion.cursor is null is effective and repaints' , ( |
1736 | WidgetTester tester, |
1737 | ) async { |
1738 | final List<String> logEnters = <String>[]; |
1739 | final List<String> logPaints = <String>[]; |
1740 | |
1741 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
1742 | await gesture.addPointer(location: const Offset(100, 100)); |
1743 | |
1744 | void onPaintChild() { |
1745 | logPaints.add('paint' ); |
1746 | } |
1747 | |
1748 | await tester.pumpWidget( |
1749 | _Scaffold( |
1750 | topLeft: SizedBox( |
1751 | height: 10, |
1752 | width: 10, |
1753 | child: MouseRegion( |
1754 | cursor: SystemMouseCursors.forbidden, |
1755 | child: MouseRegion( |
1756 | cursor: SystemMouseCursors.text, |
1757 | onEnter: (_) { |
1758 | logEnters.add('enter' ); |
1759 | }, |
1760 | child: CustomPaint(painter: _DelegatedPainter(onPaint: onPaintChild)), |
1761 | ), |
1762 | ), |
1763 | ), |
1764 | ), |
1765 | ); |
1766 | await gesture.moveTo(const Offset(5, 5)); |
1767 | |
1768 | expect(logPaints, <String>['paint' ]); |
1769 | expect(logEnters, <String>['enter' ]); |
1770 | expect( |
1771 | RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), |
1772 | SystemMouseCursors.text, |
1773 | ); |
1774 | logPaints.clear(); |
1775 | logEnters.clear(); |
1776 | |
1777 | await tester.pumpWidget( |
1778 | _Scaffold( |
1779 | topLeft: SizedBox( |
1780 | height: 10, |
1781 | width: 10, |
1782 | child: MouseRegion( |
1783 | cursor: SystemMouseCursors.forbidden, |
1784 | child: MouseRegion( |
1785 | onEnter: (_) { |
1786 | logEnters.add('enter' ); |
1787 | }, |
1788 | child: CustomPaint(painter: _DelegatedPainter(onPaint: onPaintChild)), |
1789 | ), |
1790 | ), |
1791 | ), |
1792 | ), |
1793 | ); |
1794 | |
1795 | expect(logPaints, <String>['paint' ]); |
1796 | expect(logEnters, isEmpty); |
1797 | expect( |
1798 | RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), |
1799 | SystemMouseCursors.forbidden, |
1800 | ); |
1801 | logPaints.clear(); |
1802 | logEnters.clear(); |
1803 | |
1804 | await tester.pumpWidget( |
1805 | _Scaffold( |
1806 | topLeft: SizedBox( |
1807 | height: 10, |
1808 | width: 10, |
1809 | child: MouseRegion( |
1810 | cursor: SystemMouseCursors.forbidden, |
1811 | child: MouseRegion( |
1812 | cursor: SystemMouseCursors.text, |
1813 | child: CustomPaint(painter: _DelegatedPainter(onPaint: onPaintChild)), |
1814 | ), |
1815 | ), |
1816 | ), |
1817 | ), |
1818 | ); |
1819 | |
1820 | expect(logPaints, <String>['paint' ]); |
1821 | expect( |
1822 | RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), |
1823 | SystemMouseCursors.text, |
1824 | ); |
1825 | expect(logEnters, isEmpty); |
1826 | logPaints.clear(); |
1827 | logEnters.clear(); |
1828 | }); |
1829 | |
1830 | testWidgets('Does not trigger side effects during a reparent' , (WidgetTester tester) async { |
1831 | final List<String> logEnters = <String>[]; |
1832 | final List<String> logExits = <String>[]; |
1833 | final List<String> logCursors = <String>[]; |
1834 | |
1835 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
1836 | await gesture.addPointer(location: const Offset(100, 100)); |
1837 | tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.mouseCursor, ( |
1838 | _, |
1839 | ) async { |
1840 | logCursors.add('cursor' ); |
1841 | return null; |
1842 | }); |
1843 | |
1844 | final GlobalKey key = GlobalKey(); |
1845 | |
1846 | // Pump a row of 2 SizedBox's, each taking 50px of width. |
1847 | await tester.pumpWidget( |
1848 | _Scaffold( |
1849 | topLeft: SizedBox( |
1850 | width: 100, |
1851 | height: 50, |
1852 | child: Row( |
1853 | children: <Widget>[ |
1854 | SizedBox( |
1855 | width: 50, |
1856 | height: 50, |
1857 | child: MouseRegion( |
1858 | key: key, |
1859 | onEnter: (_) { |
1860 | logEnters.add('enter' ); |
1861 | }, |
1862 | onExit: (_) { |
1863 | logEnters.add('enter' ); |
1864 | }, |
1865 | cursor: SystemMouseCursors.click, |
1866 | ), |
1867 | ), |
1868 | const SizedBox(width: 50, height: 50), |
1869 | ], |
1870 | ), |
1871 | ), |
1872 | ), |
1873 | ); |
1874 | |
1875 | // Move to the mouse region inside the first box. |
1876 | await gesture.moveTo(const Offset(40, 5)); |
1877 | |
1878 | expect(logEnters, <String>['enter' ]); |
1879 | expect(logExits, isEmpty); |
1880 | expect(logCursors, isNotEmpty); |
1881 | expect( |
1882 | RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), |
1883 | SystemMouseCursors.click, |
1884 | ); |
1885 | logEnters.clear(); |
1886 | logExits.clear(); |
1887 | logCursors.clear(); |
1888 | |
1889 | // Move MouseRegion to the second box while resizing them so that the |
1890 | // mouse is still on the MouseRegion |
1891 | await tester.pumpWidget( |
1892 | _Scaffold( |
1893 | topLeft: SizedBox( |
1894 | width: 100, |
1895 | height: 50, |
1896 | child: Row( |
1897 | children: <Widget>[ |
1898 | const SizedBox(width: 30, height: 50), |
1899 | SizedBox( |
1900 | width: 70, |
1901 | height: 50, |
1902 | child: MouseRegion( |
1903 | key: key, |
1904 | onEnter: (_) { |
1905 | logEnters.add('enter' ); |
1906 | }, |
1907 | onExit: (_) { |
1908 | logEnters.add('enter' ); |
1909 | }, |
1910 | cursor: SystemMouseCursors.click, |
1911 | ), |
1912 | ), |
1913 | ], |
1914 | ), |
1915 | ), |
1916 | ), |
1917 | ); |
1918 | |
1919 | expect(logEnters, isEmpty); |
1920 | expect(logExits, isEmpty); |
1921 | expect(logCursors, isEmpty); |
1922 | expect( |
1923 | RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), |
1924 | SystemMouseCursors.click, |
1925 | ); |
1926 | }); |
1927 | |
1928 | testWidgets("RenderMouseRegion's debugFillProperties when default" , (WidgetTester tester) async { |
1929 | final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); |
1930 | |
1931 | final RenderMouseRegion renderMouseRegion = RenderMouseRegion(); |
1932 | addTearDown(renderMouseRegion.dispose); |
1933 | |
1934 | renderMouseRegion.debugFillProperties(builder); |
1935 | |
1936 | final List<String> description = builder.properties |
1937 | .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) |
1938 | .map((DiagnosticsNode node) => node.toString()) |
1939 | .toList(); |
1940 | |
1941 | expect(description, <String>[ |
1942 | 'parentData: MISSING' , |
1943 | 'constraints: MISSING' , |
1944 | 'size: MISSING' , |
1945 | 'behavior: opaque' , |
1946 | 'listeners: <none>' , |
1947 | ]); |
1948 | }); |
1949 | |
1950 | testWidgets("RenderMouseRegion's debugFillProperties when full" , (WidgetTester tester) async { |
1951 | final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); |
1952 | |
1953 | final RenderErrorBox renderErrorBox = RenderErrorBox(); |
1954 | addTearDown(renderErrorBox.dispose); |
1955 | |
1956 | final RenderMouseRegion renderMouseRegion = RenderMouseRegion( |
1957 | onEnter: (PointerEnterEvent event) {}, |
1958 | onExit: (PointerExitEvent event) {}, |
1959 | onHover: (PointerHoverEvent event) {}, |
1960 | cursor: SystemMouseCursors.click, |
1961 | validForMouseTracker: false, |
1962 | child: renderErrorBox, |
1963 | ); |
1964 | addTearDown(renderMouseRegion.dispose); |
1965 | |
1966 | renderMouseRegion.debugFillProperties(builder); |
1967 | |
1968 | final List<String> description = builder.properties |
1969 | .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) |
1970 | .map((DiagnosticsNode node) => node.toString()) |
1971 | .toList(); |
1972 | |
1973 | expect(description, <String>[ |
1974 | 'parentData: MISSING' , |
1975 | 'constraints: MISSING' , |
1976 | 'size: MISSING' , |
1977 | 'behavior: opaque' , |
1978 | 'listeners: enter, hover, exit' , |
1979 | 'cursor: SystemMouseCursor(click)' , |
1980 | 'invalid for MouseTracker' , |
1981 | ]); |
1982 | }); |
1983 | |
1984 | testWidgets('No new frames are scheduled when mouse moves without triggering callbacks' , ( |
1985 | WidgetTester tester, |
1986 | ) async { |
1987 | await tester.pumpWidget( |
1988 | Center( |
1989 | child: MouseRegion( |
1990 | child: const SizedBox(width: 100.0, height: 100.0), |
1991 | onEnter: (PointerEnterEvent details) {}, |
1992 | onHover: (PointerHoverEvent details) {}, |
1993 | onExit: (PointerExitEvent details) {}, |
1994 | ), |
1995 | ), |
1996 | ); |
1997 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
1998 | await gesture.addPointer(location: const Offset(400.0, 300.0)); |
1999 | await tester.pumpAndSettle(); |
2000 | await gesture.moveBy(const Offset(10.0, 10.0)); |
2001 | expect(tester.binding.hasScheduledFrame, isFalse); |
2002 | }); |
2003 | |
2004 | // Regression test for https://github.com/flutter/flutter/issues/67044 |
2005 | testWidgets('Handle mouse events should ignore the detached MouseTrackerAnnotation' , ( |
2006 | WidgetTester tester, |
2007 | ) async { |
2008 | await tester.pumpWidget( |
2009 | MaterialApp( |
2010 | home: Center( |
2011 | child: Draggable<int>( |
2012 | feedback: Container(width: 20, height: 20, color: Colors.blue), |
2013 | childWhenDragging: Container(width: 20, height: 20, color: Colors.yellow), |
2014 | child: ElevatedButton(child: const Text('Drag me' ), onPressed: () {}), |
2015 | ), |
2016 | ), |
2017 | ), |
2018 | ); |
2019 | |
2020 | // Long press the button with mouse. |
2021 | final Offset textFieldPos = tester.getCenter(find.byType(Text)); |
2022 | final TestGesture gesture = await tester.startGesture( |
2023 | textFieldPos, |
2024 | kind: PointerDeviceKind.mouse, |
2025 | ); |
2026 | await tester.pump(const Duration(seconds: 2)); |
2027 | await tester.pumpAndSettle(); |
2028 | |
2029 | // Drag the Draggable Widget will replace the child with [childWhenDragging]. |
2030 | await gesture.moveBy(const Offset(10.0, 10.0)); |
2031 | await tester.pump(); // Trigger detach the button. |
2032 | |
2033 | // Continue drag mouse should not trigger any assert. |
2034 | await gesture.moveBy(const Offset(10.0, 10.0)); |
2035 | |
2036 | // Dispose gesture |
2037 | await gesture.cancel(); |
2038 | expect(tester.takeException(), isNull); |
2039 | }); |
2040 | |
2041 | testWidgets('stylus input works' , (WidgetTester tester) async { |
2042 | bool onEnter = false; |
2043 | bool onExit = false; |
2044 | bool onHover = false; |
2045 | await tester.pumpWidget( |
2046 | MaterialApp( |
2047 | home: Scaffold( |
2048 | body: MouseRegion( |
2049 | onEnter: (_) => onEnter = true, |
2050 | onExit: (_) => onExit = true, |
2051 | onHover: (_) => onHover = true, |
2052 | child: const SizedBox(width: 10.0, height: 10.0), |
2053 | ), |
2054 | ), |
2055 | ), |
2056 | ); |
2057 | |
2058 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.stylus); |
2059 | await gesture.addPointer(location: const Offset(20.0, 20.0)); |
2060 | await tester.pump(); |
2061 | |
2062 | expect(onEnter, false); |
2063 | expect(onHover, false); |
2064 | expect(onExit, false); |
2065 | |
2066 | await gesture.moveTo(const Offset(5.0, 5.0)); |
2067 | await tester.pump(); |
2068 | |
2069 | expect(onEnter, true); |
2070 | expect(onHover, true); |
2071 | expect(onExit, false); |
2072 | |
2073 | await gesture.moveTo(const Offset(20.0, 20.0)); |
2074 | await tester.pump(); |
2075 | |
2076 | expect(onEnter, true); |
2077 | expect(onHover, true); |
2078 | expect(onExit, true); |
2079 | }); |
2080 | } |
2081 | |
2082 | // Render widget `topLeft` at the top-left corner, stacking on top of the widget |
2083 | // `background`. |
2084 | class _Scaffold extends StatelessWidget { |
2085 | const _Scaffold({this.topLeft, this.background}); |
2086 | |
2087 | final Widget? topLeft; |
2088 | final Widget? background; |
2089 | |
2090 | @override |
2091 | Widget build(BuildContext context) { |
2092 | return Directionality( |
2093 | textDirection: TextDirection.ltr, |
2094 | child: Stack( |
2095 | children: <Widget>[ |
2096 | if (background != null) background!, |
2097 | Align(alignment: Alignment.topLeft, child: topLeft), |
2098 | ], |
2099 | ), |
2100 | ); |
2101 | } |
2102 | } |
2103 | |
2104 | class _DelegatedPainter extends CustomPainter { |
2105 | _DelegatedPainter({this.key, required this.onPaint}); |
2106 | final Key? key; |
2107 | final VoidCallback onPaint; |
2108 | |
2109 | @override |
2110 | void paint(Canvas canvas, Size size) { |
2111 | onPaint(); |
2112 | } |
2113 | |
2114 | @override |
2115 | bool shouldRepaint(CustomPainter oldDelegate) => |
2116 | !(oldDelegate is _DelegatedPainter && key == oldDelegate.key); |
2117 | } |
2118 | |
2119 | class _HoverClientWithClosures extends StatefulWidget { |
2120 | const _HoverClientWithClosures(); |
2121 | |
2122 | @override |
2123 | _HoverClientWithClosuresState createState() => _HoverClientWithClosuresState(); |
2124 | } |
2125 | |
2126 | class _HoverClientWithClosuresState extends State<_HoverClientWithClosures> { |
2127 | bool _hovering = false; |
2128 | |
2129 | @override |
2130 | Widget build(BuildContext context) { |
2131 | return Directionality( |
2132 | textDirection: TextDirection.ltr, |
2133 | child: MouseRegion( |
2134 | onEnter: (PointerEnterEvent _) { |
2135 | setState(() { |
2136 | _hovering = true; |
2137 | }); |
2138 | }, |
2139 | onExit: (PointerExitEvent _) { |
2140 | setState(() { |
2141 | _hovering = false; |
2142 | }); |
2143 | }, |
2144 | child: Text(_hovering ? 'HOVERING' : 'not hovering' ), |
2145 | ), |
2146 | ); |
2147 | } |
2148 | } |
2149 | |
2150 | // A column that aligns to the top left. |
2151 | class _ColumnContainer extends StatelessWidget { |
2152 | const _ColumnContainer({required this.children}); |
2153 | |
2154 | final List<Widget> children; |
2155 | |
2156 | @override |
2157 | Widget build(BuildContext context) { |
2158 | return Directionality( |
2159 | textDirection: TextDirection.ltr, |
2160 | child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: children), |
2161 | ); |
2162 | } |
2163 | } |
2164 | |