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/// @docImport 'package:flutter/material.dart';
6library;
7
8import 'dart:ui' as ui;
9
10import 'package:flutter/foundation.dart';
11import 'package:flutter/rendering.dart';
12import 'package:flutter/scheduler.dart';
13import 'package:flutter/services.dart';
14
15import 'actions.dart';
16import 'basic.dart';
17import 'focus_manager.dart';
18import 'focus_traversal.dart';
19import 'framework.dart';
20import 'media_query.dart';
21import 'overlay.dart';
22import 'scroll_position.dart';
23import 'scrollable.dart';
24import 'shortcuts.dart';
25import 'tap_region.dart';
26
27// Examples can assume:
28// late BuildContext context;
29// late List menuItems;
30// late RawMenuOverlayInfo info;
31
32const bool _kDebugMenus = false;
33
34const Map<ShortcutActivator, Intent> _kMenuTraversalShortcuts = <ShortcutActivator, Intent>{
35 SingleActivator(LogicalKeyboardKey.gameButtonA): ActivateIntent(),
36 SingleActivator(LogicalKeyboardKey.escape): DismissIntent(),
37 SingleActivator(LogicalKeyboardKey.arrowDown): DirectionalFocusIntent(TraversalDirection.down),
38 SingleActivator(LogicalKeyboardKey.arrowUp): DirectionalFocusIntent(TraversalDirection.up),
39 SingleActivator(LogicalKeyboardKey.arrowLeft): DirectionalFocusIntent(TraversalDirection.left),
40 SingleActivator(LogicalKeyboardKey.arrowRight): DirectionalFocusIntent(TraversalDirection.right),
41};
42
43/// Anchor and menu information passed to [RawMenuAnchor].
44@immutable
45class RawMenuOverlayInfo {
46 /// Creates a [RawMenuOverlayInfo].
47 const RawMenuOverlayInfo({
48 required this.anchorRect,
49 required this.overlaySize,
50 required this.tapRegionGroupId,
51 this.position,
52 });
53
54 /// The position of the anchor widget that the menu is attached to, relative to
55 /// the nearest ancestor [Overlay] when [RawMenuAnchor.useRootOverlay] is false,
56 /// or the root [Overlay] when [RawMenuAnchor.useRootOverlay] is true.
57 final ui.Rect anchorRect;
58
59 /// The [Size] of the overlay that the menu is being shown in.
60 final ui.Size overlaySize;
61
62 /// The `position` argument passed to [MenuController.open].
63 ///
64 /// The position should be used to offset the menu relative to the top-left
65 /// corner of the anchor.
66 final Offset? position;
67
68 /// The [TapRegion.groupId] of the [TapRegion] that wraps widgets in this menu
69 /// system.
70 final Object tapRegionGroupId;
71
72 @override
73 bool operator ==(Object other) {
74 if (identical(this, other)) {
75 return true;
76 }
77
78 if (other.runtimeType != runtimeType) {
79 return false;
80 }
81
82 return other is RawMenuOverlayInfo &&
83 other.anchorRect == anchorRect &&
84 other.overlaySize == overlaySize &&
85 other.position == position &&
86 other.tapRegionGroupId == tapRegionGroupId;
87 }
88
89 @override
90 int get hashCode {
91 return Object.hash(anchorRect, overlaySize, position, tapRegionGroupId);
92 }
93}
94
95/// Signature for the builder function used by [RawMenuAnchor.overlayBuilder] to
96/// build a menu's overlay.
97///
98/// The `context` is the context that the overlay is being built in.
99///
100/// The `info` describes the anchor's [Rect], the [Size] of the overlay,
101/// the [TapRegion.groupId] used by members of the menu system, and the
102/// `position` argument passed to [MenuController.open].
103typedef RawMenuAnchorOverlayBuilder =
104 Widget Function(BuildContext context, RawMenuOverlayInfo info);
105
106/// Signature for the builder function used by [RawMenuAnchor.builder] to build
107/// the widget that the [RawMenuAnchor] surrounds.
108///
109/// The `context` is the context in which the anchor is being built.
110///
111/// The `controller` is the [MenuController] that can be used to open and close
112/// the menu.
113///
114/// The `child` is an optional child supplied as the [RawMenuAnchor.child]
115/// attribute. The child is intended to be incorporated in the result of the
116/// function.
117typedef RawMenuAnchorChildBuilder =
118 Widget Function(BuildContext context, MenuController controller, Widget? child);
119
120/// Signature for the callback used by [RawMenuAnchor.onOpenRequested] to
121/// intercept requests to open a menu.
122///
123/// See [RawMenuAnchor.onOpenRequested] for more information.
124typedef RawMenuAnchorOpenRequestedCallback =
125 void Function(Offset? position, VoidCallback showOverlay);
126
127/// Signature for the callback used by [RawMenuAnchor.onCloseRequested] to
128/// intercept requests to close a menu.
129///
130/// See [RawMenuAnchor.onCloseRequested] for more information.
131typedef RawMenuAnchorCloseRequestedCallback = void Function(VoidCallback hideOverlay);
132
133// An InheritedWidget used to notify anchor descendants when a menu opens
134// and closes, and to pass the anchor's controller to descendants.
135class _MenuControllerScope extends InheritedWidget {
136 const _MenuControllerScope({
137 required this.isOpen,
138 required this.controller,
139 required super.child,
140 });
141
142 final bool isOpen;
143 final MenuController controller;
144
145 @override
146 bool updateShouldNotify(_MenuControllerScope oldWidget) {
147 return isOpen != oldWidget.isOpen;
148 }
149}
150
151/// A widget that wraps a child and anchors a floating menu.
152///
153/// The child can be any widget, but is typically a button, a text field, or, in
154/// the case of context menus, the entire screen.
155///
156/// The menu overlay of a [RawMenuAnchor] is shown by calling
157/// [MenuController.open] on an attached [MenuController].
158///
159/// When a [RawMenuAnchor] is opened, [overlayBuilder] is called to construct
160/// the menu contents within an [Overlay]. The [Overlay] allows the menu to
161/// "float" on top of other widgets. The `info` argument passed to
162/// [overlayBuilder] provides the anchor's [Rect], the [Size] of the overlay,
163/// the [TapRegion.groupId] used by members of the menu system, and the
164/// `position` argument passed to [MenuController.open].
165///
166/// If [MenuController.open] is called with a `position` argument, it will be
167/// passed to the `info` argument of the `overlayBuilder` function.
168///
169/// The [RawMenuAnchor] does not manage semantics and focus of the menu.
170///
171/// ### Adding animations to menus
172///
173/// A [RawMenuAnchor] has no knowledge of animations, as evident from its APIs,
174/// which don't involve [AnimationController] at all. It only knows whether the
175/// overlay is shown or hidden.
176///
177/// If another widget intends to implement a menu with opening and closing
178/// transitions, [RawMenuAnchor]'s overlay should remain visible throughout both
179/// the opening and closing animation durations.
180///
181/// This means that the `showOverlay` callback passed to [onOpenRequested]
182/// should be called before the first frame of the opening animation.
183/// Conversely, `hideOverlay` within [onCloseRequested] should only be called
184/// after the closing animation has completed.
185///
186/// This also means that, if [MenuController.open] is called while the overlay
187/// is already visible, [RawMenuAnchor] has no way of knowing whether the menu
188/// is currently opening, closing, or stably displayed. The parent widget will
189/// need to manage additional information (such as the state of an
190/// [AnimationController]) to determine how to respond in such scenarios.
191///
192/// To programmatically control a [RawMenuAnchor], like opening or closing it,
193/// or checking its state, you can get its associated [MenuController]. Use
194/// `MenuController.maybeOf(BuildContext context)` to retrieve the controller
195/// for the closest [RawMenuAnchor] ancestor of a given `BuildContext`. More
196/// detailed usage of [MenuController] is available in its class documentation.
197///
198/// {@tool dartpad}
199///
200/// This example uses a [RawMenuAnchor] to build a basic select menu with four
201/// items.
202///
203/// ** See code in examples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.0.dart **
204/// {@end-tool}
205///
206/// {@tool dartpad}
207///
208/// This example uses [RawMenuAnchor.onOpenRequested] and
209/// [RawMenuAnchor.onCloseRequested] to build an animated menu.
210///
211/// ** See code in examples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.2.dart **
212/// {@end-tool}
213///
214/// {@tool dartpad}
215///
216/// This example uses [RawMenuAnchor.onOpenRequested] and
217/// [RawMenuAnchor.onCloseRequested] to build an animated nested menu.
218///
219/// ** See code in examples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.3.dart **
220/// {@end-tool}
221class RawMenuAnchor extends StatefulWidget {
222 /// A [RawMenuAnchor] that delegates overlay construction to an [overlayBuilder].
223 ///
224 /// The [overlayBuilder] must not be null.
225 const RawMenuAnchor({
226 super.key,
227 this.childFocusNode,
228 this.consumeOutsideTaps = false,
229 this.onOpen,
230 this.onClose,
231 this.onOpenRequested = _defaultOnOpenRequested,
232 this.onCloseRequested = _defaultOnCloseRequested,
233 this.useRootOverlay = false,
234 this.builder,
235 required this.controller,
236 required this.overlayBuilder,
237 this.child,
238 });
239
240 /// Called when the menu overlay is shown.
241 ///
242 /// When [MenuController.open] is called, [onOpenRequested] is invoked with a
243 /// `showOverlay` callback that, when called, shows the menu overlay and
244 /// triggers [onOpen].
245 ///
246 /// The default implementation of [onOpenRequested] calls `showOverlay`
247 /// synchronously, thereby calling [onOpen] synchronously. In this case,
248 /// [onOpen] is called regardless of whether the menu overlay is already
249 /// showing.
250 ///
251 /// Custom implementations of [onOpenRequested] can delay the call to
252 /// `showOverlay`, or not call it at all, in which case [onOpen] will not be
253 /// called. Calling `showOverlay` after disposal is a no-op, and will not
254 /// trigger [onOpen].
255 ///
256 /// A typical usage is to respond when the menu first becomes interactive,
257 /// such as by setting focus to a menu item.
258 final VoidCallback? onOpen;
259
260 /// Called when the menu overlay is hidden.
261 ///
262 /// When [MenuController.close] is called, [onCloseRequested] is invoked with
263 /// a `hideOverlay` callback that, when called, hides the menu overlay and
264 /// triggers [onClose].
265 ///
266 /// The default implementation of [onCloseRequested] calls `hideOverlay`
267 /// synchronously, thereby calling [onClose] synchronously. In this case,
268 /// [onClose] is called regardless of whether the menu overlay is already
269 /// hidden.
270 ///
271 /// Custom implementations of [onCloseRequested] can delay the call to
272 /// `hideOverlay` or not call it at all, in which case [onClose] will not be
273 /// called. Calling `hideOverlay` after disposal is a no-op, and will not
274 /// trigger [onClose].
275 final VoidCallback? onClose;
276
277 /// Called when a request is made to open the menu.
278 ///
279 /// This callback is triggered every time [MenuController.open] is called,
280 /// even when the menu overlay is already showing. As a result, this callback
281 /// is a good place to begin menu opening animations, or observe when a menu
282 /// is repositioned.
283 ///
284 /// After an open request is intercepted, the `showOverlay` callback should be
285 /// called when the menu overlay (the widget built by [overlayBuilder]) is
286 /// ready to be shown. This can occur immediately (the default behavior), or
287 /// after a delay. Calling `showOverlay` sets [MenuController.isOpen] to true,
288 /// builds (or rebuilds) the overlay widget, and shows the menu overlay at the
289 /// front of the overlay stack.
290 ///
291 /// If `showOverlay` is not called, the menu will stay hidden. Calling
292 /// `showOverlay` after disposal is a no-op, meaning it will not trigger
293 /// [onOpen] or show the menu overlay.
294 ///
295 /// If a [RawMenuAnchor] is used in a themed menu that plays an opening
296 /// animation, the themed menu should show the overlay before starting the
297 /// opening animation, since the animation plays on the overlay itself.
298 ///
299 /// The `position` argument is the `position` that [MenuController.open] was
300 /// called with.
301 ///
302 /// A typical [onOpenRequested] consists of the following steps:
303 ///
304 /// 1. Optional delay.
305 /// 2. Call `showOverlay` (whose call chain eventually invokes [onOpen]).
306 /// 3. Optionally start the opening animation.
307 ///
308 /// Defaults to a callback that immediately shows the menu.
309 final RawMenuAnchorOpenRequestedCallback onOpenRequested;
310
311 /// Called when a request is made to close the menu.
312 ///
313 /// This callback is triggered every time [MenuController.close] is called,
314 /// regardless of whether the overlay is already hidden. As a result, this
315 /// callback can be used to add a delay or a closing animation before the menu
316 /// is hidden.
317 ///
318 /// If the menu is not closed, this callback will also be called when the root
319 /// menu anchor is scrolled and when the screen is resized.
320 ///
321 /// After a close request is intercepted and closing behaviors have completed,
322 /// the `hideOverlay` callback should be called. This callback sets
323 /// [MenuController.isOpen] to false and hides the menu overlay widget. If the
324 /// [RawMenuAnchor] is used in a themed menu that plays a closing animation,
325 /// `hideOverlay` should be called after the closing animation has ended,
326 /// since the animation plays on the overlay itself. This means that
327 /// [MenuController.isOpen] will stay true while closing animations are
328 /// running.
329 ///
330 /// Calling `hideOverlay` after disposal is a no-op, meaning it will not
331 /// trigger [onClose] or hide the menu overlay.
332 ///
333 /// Typically, [onCloseRequested] consists of the following steps:
334 ///
335 /// 1. Optionally start the closing animation and wait for it to complete.
336 /// 2. Call `hideOverlay` (whose call chain eventually invokes [onClose]).
337 ///
338 /// Throughout the closing sequence, menus should typically not be focusable
339 /// or interactive.
340 ///
341 /// Defaults to a callback that immediately hides the menu.
342 final RawMenuAnchorCloseRequestedCallback onCloseRequested;
343
344 /// A builder that builds the widget that this [RawMenuAnchor] surrounds.
345 ///
346 /// Typically, this is a button used to open the menu by calling
347 /// [MenuController.open] on the `controller` passed to the builder.
348 ///
349 /// If not supplied, then the [RawMenuAnchor] will be the size that its parent
350 /// allocates for it.
351 final RawMenuAnchorChildBuilder? builder;
352
353 /// The optional child to be passed to the [builder].
354 ///
355 /// Supply this child if there is a portion of the widget tree built in
356 /// [builder] that doesn't depend on the `controller` or `context` supplied to
357 /// the [builder]. It will be more efficient, since Flutter doesn't then need
358 /// to rebuild this child when those change.
359 final Widget? child;
360
361 /// Called to build and position the menu overlay.
362 ///
363 /// The [overlayBuilder] function is passed a [RawMenuOverlayInfo] object that
364 /// defines the anchor's [Rect], the [Size] of the overlay, the
365 /// [TapRegion.groupId] for the menu system, and the position [Offset] passed
366 /// to [MenuController.open].
367 ///
368 /// To ensure taps are properly consumed, the
369 /// [RawMenuOverlayInfo.tapRegionGroupId] should be passed to a [TapRegion]
370 /// widget that wraps the menu panel.
371 ///
372 /// ```dart
373 /// TapRegion(
374 /// groupId: info.tapRegionGroupId,
375 /// onTapOutside: (PointerDownEvent event) {
376 /// MenuController.maybeOf(context)?.close();
377 /// },
378 /// child: Column(children: menuItems),
379 /// )
380 /// ```
381 final RawMenuAnchorOverlayBuilder overlayBuilder;
382
383 /// {@template flutter.widgets.RawMenuAnchor.useRootOverlay}
384 /// Whether the menu panel should be rendered in the root [Overlay].
385 ///
386 /// When true, the menu is mounted in the root overlay. Rendering the menu in
387 /// the root overlay prevents the menu from being obscured by other widgets.
388 ///
389 /// When false, the menu is rendered in the nearest ancestor [Overlay].
390 ///
391 /// Submenus will always use the same overlay as their top-level ancestor, so
392 /// setting a [useRootOverlay] value on a submenu will have no effect.
393 /// {@endtemplate}
394 ///
395 /// Defaults to false on overlay menus.
396 final bool useRootOverlay;
397
398 /// The [FocusNode] attached to the widget that takes focus when the
399 /// menu is opened or closed.
400 ///
401 /// If not supplied, the anchor will not retain focus when the menu is opened.
402 final FocusNode? childFocusNode;
403
404 /// Whether a tap event that closes the menu will be permitted to continue on
405 /// to the gesture arena.
406 ///
407 /// If false, then tapping outside of a menu when the menu is open will both
408 /// close the menu, and allow the tap to participate in the gesture arena.
409 ///
410 /// If true, then it will only close the menu, and the tap event will be
411 /// consumed.
412 ///
413 /// Defaults to false.
414 final bool consumeOutsideTaps;
415
416 /// A [MenuController] that allows opening and closing of the menu from other
417 /// widgets.
418 final MenuController controller;
419
420 static void _defaultOnOpenRequested(Offset? position, VoidCallback showOverlay) {
421 showOverlay();
422 }
423
424 static void _defaultOnCloseRequested(VoidCallback hideOverlay) {
425 hideOverlay();
426 }
427
428 @override
429 State<RawMenuAnchor> createState() => _RawMenuAnchorState();
430
431 @override
432 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
433 super.debugFillProperties(properties);
434 properties.add(ObjectFlagProperty<FocusNode>.has('focusNode', childFocusNode));
435 properties.add(
436 FlagProperty(
437 'useRootOverlay',
438 value: useRootOverlay,
439 ifFalse: 'use nearest overlay',
440 ifTrue: 'use root overlay',
441 ),
442 );
443 }
444}
445
446// Base mixin that provides the common interface and state for both types of
447// [RawMenuAnchor]s, [RawMenuAnchor] and [RawMenuAnchorGroup].
448@optionalTypeArgs
449mixin _RawMenuAnchorBaseMixin<T extends StatefulWidget> on State<T> {
450 final List<_RawMenuAnchorBaseMixin> _anchorChildren = <_RawMenuAnchorBaseMixin>[];
451 _RawMenuAnchorBaseMixin? _parent;
452 ScrollPosition? _scrollPosition;
453 Size? _viewSize;
454
455 /// Whether this [_RawMenuAnchorBaseMixin] is the top node of the menu tree.
456 @protected
457 bool get isRoot => _parent == null;
458
459 /// The [MenuController] that is used by the [_RawMenuAnchorBaseMixin].
460 ///
461 /// If an overriding widget does not provide a [MenuController], then
462 /// [_RawMenuAnchorBaseMixin] will create and manage its own.
463 MenuController get menuController;
464
465 /// Whether this submenu's overlay is visible.
466 @protected
467 bool get isOpen;
468
469 /// The root of the menu tree that this [RawMenuAnchor] is in.
470 @protected
471 _RawMenuAnchorBaseMixin get root {
472 _RawMenuAnchorBaseMixin anchor = this;
473 while (anchor._parent != null) {
474 anchor = anchor._parent!;
475 }
476 return anchor;
477 }
478
479 @override
480 void initState() {
481 super.initState();
482 menuController._attach(this);
483 }
484
485 @override
486 void didChangeDependencies() {
487 super.didChangeDependencies();
488 final _RawMenuAnchorBaseMixin? newParent = MenuController.maybeOf(context)?._anchor;
489 if (newParent != _parent) {
490 assert(
491 newParent != this,
492 'A MenuController should only be attached to one anchor at a time.',
493 );
494 _parent?._removeChild(this);
495 _parent = newParent;
496 _parent?._addChild(this);
497 }
498
499 if (isRoot) {
500 _scrollPosition?.isScrollingNotifier.removeListener(_handleScroll);
501 _scrollPosition = Scrollable.maybeOf(context)?.position;
502 _scrollPosition?.isScrollingNotifier.addListener(_handleScroll);
503
504 final Size newSize = MediaQuery.sizeOf(context);
505 if (_viewSize != null && newSize != _viewSize && isOpen) {
506 // Close the menus if the view changes size.
507 handleCloseRequest();
508 }
509 _viewSize = newSize;
510 }
511 }
512
513 @override
514 void dispose() {
515 assert(_debugMenuInfo('Disposing of $this'));
516 if (isOpen) {
517 close(inDispose: true);
518 }
519
520 _parent?._removeChild(this);
521 _parent = null;
522 _anchorChildren.clear();
523 menuController._detach(this);
524 super.dispose();
525 }
526
527 void _addChild(_RawMenuAnchorBaseMixin child) {
528 assert(isRoot || _debugMenuInfo('Added root child: $child'));
529 assert(!_anchorChildren.contains(child));
530 _anchorChildren.add(child);
531 assert(_debugMenuInfo('Added:\n${child.widget.toStringDeep()}'));
532 assert(_debugMenuInfo('Tree:\n${widget.toStringDeep()}'));
533 }
534
535 void _removeChild(_RawMenuAnchorBaseMixin child) {
536 assert(isRoot || _debugMenuInfo('Removed root child: $child'));
537 assert(_anchorChildren.contains(child));
538 assert(_debugMenuInfo('Removing:\n${child.widget.toStringDeep()}'));
539 _anchorChildren.remove(child);
540 assert(_debugMenuInfo('Tree:\n${widget.toStringDeep()}'));
541 }
542
543 void _handleScroll() {
544 // If an ancestor scrolls, and we're a root anchor, then close the menus.
545 // Don't just close it on *any* scroll, since we want to be able to scroll
546 // menus themselves if they're too big for the view.
547 if (isOpen) {
548 handleCloseRequest();
549 }
550 }
551
552 void _childChangedOpenState() {
553 _parent?._childChangedOpenState();
554 if (SchedulerBinding.instance.schedulerPhase != SchedulerPhase.persistentCallbacks) {
555 setState(() {
556 // Mark dirty now, but only if not in a build.
557 });
558 } else {
559 SchedulerBinding.instance.addPostFrameCallback((Duration timestamp) {
560 setState(() {
561 // Mark dirty
562 });
563 });
564 }
565 }
566
567 /// Open the menu, optionally at a position relative to the [RawMenuAnchor].
568 ///
569 /// Call this when the menu overlay should be shown and added to the widget
570 /// tree.
571 ///
572 /// The optional `position` argument should specify the location of the menu
573 /// in the local coordinates of the [RawMenuAnchor].
574 @protected
575 void open({Offset? position});
576
577 /// Close the menu and all of its children.
578 ///
579 /// Call this when the menu overlay should be hidden and removed from the
580 /// widget tree.
581 ///
582 /// If `inDispose` is true, this method call was triggered by the widget being
583 /// unmounted.
584 @protected
585 void close({bool inDispose = false});
586
587 /// Implemented by subclasses to define what to do when [MenuController.open]
588 /// is called.
589 ///
590 /// This method should not be directly called by subclasses. Its call chain
591 /// should eventually invoke `_RawMenuAnchorBaseMixin.open`
592 @protected
593 void handleOpenRequest({Offset? position});
594
595 /// Implemented by subclasses to define what to do when [MenuController.close]
596 /// is called.
597 ///
598 /// This method should not be directly called by subclasses. Its call chain
599 /// should eventually invoke `_RawMenuAnchorBaseMixin.close`.
600 @protected
601 void handleCloseRequest();
602
603 /// Request that the submenus of this menu be closed.
604 ///
605 /// By default, this method will call [handleCloseRequest] on each child of this
606 /// menu, which will trigger the closing sequence of each child.
607 ///
608 /// If `inDispose` is true, this method was triggered by the widget being
609 /// unmounted.
610 @protected
611 void closeChildren({bool inDispose = false}) {
612 assert(_debugMenuInfo('Closing children of $this${inDispose ? ' (dispose)' : ''}'));
613 for (final _RawMenuAnchorBaseMixin child in List<_RawMenuAnchorBaseMixin>.of(_anchorChildren)) {
614 if (inDispose) {
615 child.close(inDispose: inDispose);
616 } else {
617 child.handleCloseRequest();
618 }
619 }
620 }
621
622 /// Handles taps outside of the menu surface.
623 ///
624 /// By default, this closes this submenu's children.
625 @protected
626 void handleOutsideTap(PointerDownEvent pointerDownEvent) {
627 assert(_debugMenuInfo('Tapped Outside $menuController'));
628 if (isOpen) {
629 closeChildren();
630 }
631 }
632
633 // Used to build the anchor widget in subclasses.
634 @protected
635 Widget buildAnchor(BuildContext context);
636
637 @override
638 @nonVirtual
639 Widget build(BuildContext context) {
640 return _MenuControllerScope(
641 isOpen: isOpen,
642 controller: menuController,
643 child: Actions(
644 actions: <Type, Action<Intent>>{
645 // Check if open to allow DismissIntent to bubble when the menu is
646 // closed.
647 if (isOpen) DismissIntent: DismissMenuAction(controller: menuController),
648 },
649 child: Builder(builder: buildAnchor),
650 ),
651 );
652 }
653
654 @override
655 String toString({DiagnosticLevel? minLevel}) => describeIdentity(this);
656}
657
658class _RawMenuAnchorState extends State<RawMenuAnchor> with _RawMenuAnchorBaseMixin<RawMenuAnchor> {
659 // The global key used to determine the bounding rect for the anchor.
660 final GlobalKey _anchorKey = GlobalKey<_RawMenuAnchorState>(
661 debugLabel: kReleaseMode ? null : 'MenuAnchor',
662 );
663 final OverlayPortalController _overlayController = OverlayPortalController(
664 debugLabel: kReleaseMode ? null : 'MenuAnchor controller',
665 );
666
667 Offset? _menuPosition;
668 bool get _isRootOverlayAnchor => _parent is! _RawMenuAnchorState;
669
670 // If we are a nested menu, we still want to use the same overlay as the
671 // root menu.
672 bool get useRootOverlay {
673 if (_parent case _RawMenuAnchorState(useRootOverlay: final bool useRoot)) {
674 return useRoot;
675 }
676
677 assert(_isRootOverlayAnchor);
678 return widget.useRootOverlay;
679 }
680
681 @override
682 bool get isOpen => _overlayController.isShowing;
683
684 @override
685 MenuController get menuController => widget.controller;
686
687 @override
688 void didUpdateWidget(RawMenuAnchor oldWidget) {
689 super.didUpdateWidget(oldWidget);
690 if (oldWidget.controller != widget.controller) {
691 oldWidget.controller._detach(this);
692 widget.controller._attach(this);
693 }
694 }
695
696 @override
697 void open({Offset? position}) {
698 if (!mounted) {
699 return;
700 }
701
702 if (isOpen) {
703 // The menu is already open, but we need to move to another location, so
704 // close it first.
705 close();
706 }
707
708 assert(_debugMenuInfo('Opening $this at ${position ?? Offset.zero}'));
709
710 // Close all siblings.
711 _parent?.closeChildren();
712 assert(!_overlayController.isShowing);
713 _menuPosition = position;
714 _parent?._childChangedOpenState();
715 _overlayController.show();
716
717 if (_isRootOverlayAnchor) {
718 widget.childFocusNode?.requestFocus();
719 }
720
721 widget.onOpen?.call();
722 setState(() {
723 // Mark dirty to notify MenuController dependents.
724 });
725 }
726
727 @override
728 void close({bool inDispose = false}) {
729 assert(_debugMenuInfo('Closing $this'));
730 if (!isOpen) {
731 return;
732 }
733
734 closeChildren(inDispose: inDispose);
735 // Don't hide if we're in the middle of a build.
736 if (SchedulerBinding.instance.schedulerPhase != SchedulerPhase.persistentCallbacks) {
737 _overlayController.hide();
738 } else if (!inDispose) {
739 SchedulerBinding.instance.addPostFrameCallback((_) {
740 _overlayController.hide();
741 }, debugLabel: 'MenuAnchor.hide');
742 }
743
744 if (!inDispose) {
745 // Notify that _childIsOpen changed state, but only if not
746 // currently disposing.
747 _parent?._childChangedOpenState();
748 widget.onClose?.call();
749 if (mounted &&
750 SchedulerBinding.instance.schedulerPhase != SchedulerPhase.persistentCallbacks) {
751 setState(() {
752 // Mark dirty, but only if mounted and not in a build.
753 });
754 }
755 }
756 }
757
758 @override
759 void handleOpenRequest({ui.Offset? position}) {
760 widget.onOpenRequested(position, () {
761 open(position: position);
762 });
763 }
764
765 @override
766 void handleCloseRequest() {
767 // Changes in MediaQuery.sizeOf(context) cause RawMenuAnchor to close during
768 // didChangeDependencies. When this happens, calling setState during the
769 // closing sequence (handleCloseRequest -> onCloseRequested -> hideOverlay)
770 // will throw an error, since we'd be scheduling a build during a build. We
771 // avoid this by checking if we're in a build, and if so, we schedule the
772 // close for the next frame.
773 if (SchedulerBinding.instance.schedulerPhase != SchedulerPhase.persistentCallbacks) {
774 widget.onCloseRequested(close);
775 } else {
776 SchedulerBinding.instance.addPostFrameCallback((_) {
777 if (mounted) {
778 widget.onCloseRequested(close);
779 }
780 }, debugLabel: 'RawMenuAnchor.handleCloseRequest');
781 }
782 }
783
784 Widget _buildOverlay(BuildContext context) {
785 final BuildContext anchorContext = _anchorKey.currentContext!;
786 final RenderBox overlay =
787 Overlay.of(anchorContext, rootOverlay: useRootOverlay).context.findRenderObject()!
788 as RenderBox;
789 final RenderBox anchorBox = anchorContext.findRenderObject()! as RenderBox;
790 final ui.Offset upperLeft = anchorBox.localToGlobal(Offset.zero, ancestor: overlay);
791 final ui.Offset bottomRight = anchorBox.localToGlobal(
792 anchorBox.size.bottomRight(Offset.zero),
793 ancestor: overlay,
794 );
795
796 final RawMenuOverlayInfo info = RawMenuOverlayInfo(
797 anchorRect: Rect.fromPoints(upperLeft, bottomRight),
798 overlaySize: overlay.size,
799 position: _menuPosition,
800 tapRegionGroupId: root.menuController,
801 );
802
803 return widget.overlayBuilder(context, info);
804 }
805
806 @override
807 Widget buildAnchor(BuildContext context) {
808 final Widget child = Shortcuts(
809 includeSemantics: false,
810 shortcuts: _kMenuTraversalShortcuts,
811 child: TapRegion(
812 groupId: root.menuController,
813 consumeOutsideTaps: root.isOpen && widget.consumeOutsideTaps,
814 onTapOutside: handleOutsideTap,
815 child: Builder(
816 key: _anchorKey,
817 builder: (BuildContext context) {
818 return widget.builder?.call(context, menuController, widget.child) ??
819 widget.child ??
820 const SizedBox();
821 },
822 ),
823 ),
824 );
825
826 if (useRootOverlay) {
827 return OverlayPortal.targetsRootOverlay(
828 controller: _overlayController,
829 overlayChildBuilder: _buildOverlay,
830 child: child,
831 );
832 } else {
833 return OverlayPortal(
834 controller: _overlayController,
835 overlayChildBuilder: _buildOverlay,
836 child: child,
837 );
838 }
839 }
840
841 @override
842 String toString({DiagnosticLevel? minLevel}) {
843 return describeIdentity(this);
844 }
845}
846
847/// Creates a menu anchor that is always visible and is not displayed in an
848/// [OverlayPortal].
849///
850/// A [RawMenuAnchorGroup] can be used to create a menu bar that handles
851/// external taps and keyboard shortcuts, but defines no default focus or
852/// keyboard traversal to enable more flexibility.
853///
854/// When a [MenuController] is given to a [RawMenuAnchorGroup],
855/// - [MenuController.open] has no effect.
856/// - [MenuController.close] closes all child [RawMenuAnchor]s that are open.
857/// - [MenuController.isOpen] reflects whether any child [RawMenuAnchor] is
858/// open.
859///
860/// A [child] must be provided.
861///
862/// {@tool dartpad}
863///
864/// This example uses [RawMenuAnchorGroup] to build a menu bar with four
865/// submenus. Hovering over a menu item opens its respective submenu. Selecting
866/// a menu item will close the menu and update the selected item text.
867///
868/// ** See code in examples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.1.dart **
869/// {@end-tool}
870///
871/// See also:
872/// * [MenuBar], which wraps this widget with standard layout and semantics and
873/// focus management.
874/// * [MenuAnchor], a menu anchor that follows the Material Design guidelines.
875/// * [RawMenuAnchor], a widget that defines a region attached to a floating
876/// submenu.
877class RawMenuAnchorGroup extends StatefulWidget {
878 /// Creates a [RawMenuAnchorGroup].
879 const RawMenuAnchorGroup({super.key, required this.child, required this.controller});
880
881 /// The child displayed by the [RawMenuAnchorGroup].
882 ///
883 /// To access the [MenuController] from the [child], place the child in a
884 /// builder and call [MenuController.maybeOf].
885 final Widget child;
886
887 /// An [MenuController] that allows the closing of the menu from other
888 /// widgets.
889 final MenuController controller;
890
891 @override
892 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
893 super.debugFillProperties(properties);
894 properties.add(ObjectFlagProperty<MenuController>.has('controller', controller));
895 }
896
897 @override
898 State<RawMenuAnchorGroup> createState() => _RawMenuAnchorGroupState();
899}
900
901class _RawMenuAnchorGroupState extends State<RawMenuAnchorGroup>
902 with _RawMenuAnchorBaseMixin<RawMenuAnchorGroup> {
903 @override
904 bool get isOpen => _anchorChildren.any((_RawMenuAnchorBaseMixin child) => child.isOpen);
905
906 @override
907 MenuController get menuController => widget.controller;
908
909 @override
910 void didUpdateWidget(RawMenuAnchorGroup oldWidget) {
911 super.didUpdateWidget(oldWidget);
912 if (oldWidget.controller != widget.controller) {
913 oldWidget.controller._detach(this);
914 widget.controller._attach(this);
915 }
916 }
917
918 @override
919 void close({bool inDispose = false}) {
920 if (!isOpen) {
921 return;
922 }
923
924 closeChildren(inDispose: inDispose);
925 if (!inDispose) {
926 if (SchedulerBinding.instance.schedulerPhase != SchedulerPhase.persistentCallbacks) {
927 setState(() {
928 // Mark dirty, but only if mounted and not in a build.
929 });
930 } else {
931 SchedulerBinding.instance.addPostFrameCallback((Duration timestamp) {
932 if (mounted) {
933 setState(() {
934 // Mark dirty.
935 });
936 }
937 });
938 }
939 }
940 }
941
942 @override
943 void open({Offset? position}) {
944 assert(menuController._anchor == this);
945 // Menu nodes are always open, so this is a no-op.
946 return;
947 }
948
949 @override
950 void handleCloseRequest() {
951 assert(_debugMenuInfo('Requesting close $this'));
952 close();
953 }
954
955 @override
956 void handleOpenRequest({ui.Offset? position}) {
957 assert(_debugMenuInfo('Requesting open $this'));
958 open(position: position);
959 }
960
961 @override
962 Widget buildAnchor(BuildContext context) {
963 return TapRegion(
964 groupId: root.menuController,
965 onTapOutside: handleOutsideTap,
966 child: widget.child,
967 );
968 }
969}
970
971/// A controller used to manage a menu created by a subclass of [RawMenuAnchor],
972/// such as [MenuAnchor], [MenuBar], [SubmenuButton].
973///
974/// A [MenuController] is used to control and interrogate a menu after it has
975/// been created, with methods such as [open] and [close], and state accessors
976/// like [isOpen].
977///
978/// [MenuController.maybeOf] can be used to retrieve a controller from the
979/// [BuildContext] of a widget that is a descendant of a [MenuAnchor],
980/// [MenuBar], [SubmenuButton], or [RawMenuAnchor]. Doing so will not establish
981/// a dependency relationship.
982///
983/// See also:
984///
985/// * [MenuAnchor], a menu anchor that follows the Material Design guidelines.
986/// * [MenuBar], a widget that creates a menu bar that can take an optional
987/// [MenuController].
988/// * [SubmenuButton], a widget that has a button that manages a submenu.
989/// * [RawMenuAnchor], a widget that defines a region that has submenu.
990final class MenuController {
991 // The anchor that this controller controls.
992 //
993 // This is set automatically when this `MenuController` is attached to an
994 // anchor.
995 _RawMenuAnchorBaseMixin? _anchor;
996
997 /// Whether or not the menu associated with this [MenuController] is open.
998 bool get isOpen => _anchor?.isOpen ?? false;
999
1000 /// Opens the menu that this [MenuController] is associated with.
1001 ///
1002 /// If `position` is given, then the menu will open at the position given, in
1003 /// the coordinate space of the [RawMenuAnchor] that this controller is
1004 /// attached to.
1005 ///
1006 /// If given, the `position` will override the [MenuAnchor.alignmentOffset]
1007 /// given to the [MenuAnchor].
1008 ///
1009 /// If the menu's anchor point is scrolled by an ancestor, or the view changes
1010 /// size, then any open menu will automatically close.
1011 void open({Offset? position}) {
1012 assert(_anchor != null);
1013 _anchor!.handleOpenRequest(position: position);
1014 }
1015
1016 /// Close the menu that this [MenuController] is associated with.
1017 ///
1018 /// Associating with a menu is done by passing a [MenuController] to a
1019 /// [MenuAnchor], [RawMenuAnchor], or [RawMenuAnchorGroup].
1020 ///
1021 /// If the menu's anchor point is scrolled by an ancestor, or the view changes
1022 /// size, then any open menu will automatically close.
1023 void close() {
1024 _anchor?.handleCloseRequest();
1025 }
1026
1027 /// Close the children of the menu associated with this [MenuController],
1028 /// without closing the menu itself.
1029 void closeChildren() {
1030 assert(_anchor != null);
1031 _anchor!.closeChildren();
1032 }
1033
1034 // ignore: use_setters_to_change_properties
1035 void _attach(_RawMenuAnchorBaseMixin anchor) {
1036 _anchor = anchor;
1037 }
1038
1039 void _detach(_RawMenuAnchorBaseMixin anchor) {
1040 if (_anchor == anchor) {
1041 _anchor = null;
1042 }
1043 }
1044
1045 /// Returns the [MenuController] of the ancestor [RawMenuAnchor] nearest to
1046 /// the given `context`, if one exists. Otherwise, returns null.
1047 ///
1048 /// This method will not establish a dependency relationship, so the calling
1049 /// widget will not rebuild when the menu opens and closes, nor when the
1050 /// [MenuController] changes.
1051 static MenuController? maybeOf(BuildContext context) {
1052 return context.getInheritedWidgetOfExactType<_MenuControllerScope>()?.controller;
1053 }
1054
1055 /// Returns the value of [MenuController.isOpen] of the ancestor
1056 /// [RawMenuAnchor] or [RawMenuAnchorGroup] nearest to the given `context`, if
1057 /// one exists. Otherwise, returns null.
1058 ///
1059 /// This method will establish a dependency relationship, so the calling
1060 /// widget will rebuild when the menu opens and closes.
1061 static bool? maybeIsOpenOf(BuildContext context) {
1062 return context.dependOnInheritedWidgetOfExactType<_MenuControllerScope>()?.isOpen;
1063 }
1064
1065 @override
1066 String toString() => describeIdentity(this);
1067}
1068
1069/// An action that closes all the menus associated with the given
1070/// [MenuController].
1071///
1072/// See also:
1073///
1074/// * [MenuAnchor], a material-themed widget that hosts a cascading submenu.
1075/// * [MenuBar], a widget that defines a menu bar with cascading submenus.
1076/// * [RawMenuAnchor], a widget that hosts a cascading submenu.
1077/// * [MenuController], a controller used to manage menus created by a
1078/// [RawMenuAnchor].
1079class DismissMenuAction extends DismissAction {
1080 /// Creates a [DismissMenuAction].
1081 DismissMenuAction({required this.controller});
1082
1083 /// The [MenuController] that manages the menu which should be dismissed upon
1084 /// invocation.
1085 final MenuController controller;
1086
1087 @override
1088 void invoke(DismissIntent intent) {
1089 controller._anchor!.root.handleCloseRequest();
1090 }
1091
1092 @override
1093 bool isEnabled(DismissIntent intent) {
1094 return controller._anchor != null;
1095 }
1096}
1097
1098/// A debug print function, which should only be called within an assert, like
1099/// so:
1100///
1101/// assert(_debugMenuInfo('Debug Message'));
1102///
1103/// so that the call is entirely removed in release builds.
1104///
1105/// Enable debug printing by setting [_kDebugMenus] to true at the top of the
1106/// file.
1107bool _debugMenuInfo(String message, [Iterable<String>? details]) {
1108 assert(() {
1109 if (_kDebugMenus) {
1110 debugPrint('MENU: $message');
1111 if (details != null && details.isNotEmpty) {
1112 for (final String detail in details) {
1113 debugPrint(' $detail');
1114 }
1115 }
1116 }
1117 return true;
1118 }());
1119 // Return true so that it can be easily used inside of an assert.
1120 return true;
1121}
1122