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
5import 'package:flutter/foundation.dart';
6import 'package:flutter/gestures.dart';
7import 'package:flutter/material.dart';
8import 'package:flutter/rendering.dart';
9import 'package:flutter/services.dart';
10import 'package:flutter_test/flutter_test.dart';
11
12class 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
24class 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
41class 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
51class _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
68void 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`.
2084class _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
2104class _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
2119class _HoverClientWithClosures extends StatefulWidget {
2120 const _HoverClientWithClosures();
2121
2122 @override
2123 _HoverClientWithClosuresState createState() => _HoverClientWithClosuresState();
2124}
2125
2126class _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.
2151class _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