showFlyout<T> method

Future<T?> showFlyout<T>({
  1. required WidgetBuilder builder,
  2. bool barrierDismissible = true,
  3. bool dismissWithEsc = true,
  4. bool dismissOnPointerMoveAway = false,
  5. FlyoutPlacementMode placementMode = FlyoutPlacementMode.auto,
  6. FlyoutAutoConfiguration? autoModeConfiguration,
  7. bool forceAvailableSpace = false,
  8. bool shouldConstrainToRootBounds = true,
  9. double additionalOffset = 8.0,
  10. double margin = 8.0,
  11. Color? barrierColor,
  12. NavigatorState? navigatorKey,
  13. FlyoutTransitionBuilder? transitionBuilder,
  14. Duration? transitionDuration,
  15. Offset? position,
  16. RouteSettings? settings,
  17. GestureRecognizer? barrierRecognizer,
})

Shows a flyout.

builder builds the flyout with the given context. Usually a FlyoutContent is used

If barrierDismissible is true, tapping outside of the flyout will close it.

barrierColor is the color of the barrier.

When dismissWithEsc is true, the flyout can be dismissed by pressing the ESC key.

If dismissOnPointerMoveAway is enabled, the flyout is dismissed when the cursor moves away from either the target or the flyout. It's disabled by default.

placementMode describes where the flyout will be placed. Defaults to auto

If placementMode is auto, autoModeConfiguration is taken in consideration to determine the correct placement mode

forceAvailableSpace determines whether the flyout size should be forced the available space according to the attached target. It's useful when the flyout is large but can not be on top of the target. Defaults to false

shouldConstrainToRootBounds, when true, the flyout is limited to the bounds of the closest Navigator. If false, the flyout may overflow the screen on all sides. Defaults to true

additionalOffset is the offset of the flyout around the attached target

margin is the margin of the flyout to the root bounds

If there isn't a Navigator in the tree, a navigatorKey can be used to display the flyout. If null, Navigator.of is used.

transitionBuilder builds the transition. By default, a slide-fade transition is used on vertical directions; and a fade transition in horizontal directions. The default fade animation can not be disabled.

transitionDuration configures the duration of the transition animation. By default, FluentThemeData.fastAnimationDuration is used. Set to Duration.zero to disable transitions at all

position lets you position the flyout anywhere on the screen, making it possible to create context menus. If provided, placementMode is ignored.

barrierRecognizer is a gesture recognizer that will be added to the barrier. It's useful when the flyout is used as a context menu and the barrier should be dismissed when the user clicks outside of the flyout. If this is provided, barrierDismissible is ignored.

Implementation

Future<T?> showFlyout<T>({
  required WidgetBuilder builder,
  bool barrierDismissible = true,
  bool dismissWithEsc = true,
  bool dismissOnPointerMoveAway = false,
  FlyoutPlacementMode placementMode = FlyoutPlacementMode.auto,
  FlyoutAutoConfiguration? autoModeConfiguration,
  bool forceAvailableSpace = false,
  bool shouldConstrainToRootBounds = true,
  double additionalOffset = 8.0,
  double margin = 8.0,
  Color? barrierColor,
  NavigatorState? navigatorKey,
  FlyoutTransitionBuilder? transitionBuilder,
  Duration? transitionDuration,
  Offset? position,
  RouteSettings? settings,
  GestureRecognizer? barrierRecognizer,
}) async {
  _ensureAttached();
  assert(_attachState!.mounted);

  final context = _attachState!.context;
  assert(debugCheckHasFluentTheme(context));

  final theme = FluentTheme.of(context);
  transitionDuration ??= theme.fastAnimationDuration;

  final navigator = navigatorKey ?? Navigator.of(context);

  final Offset targetOffset;
  final Size targetSize;
  final Rect targetRect;

  if (position != null) {
    targetOffset = position;
    targetSize = Size.zero;
    targetRect = Rect.zero;
  } else {
    final navigatorBox = navigator.context.findRenderObject() as RenderBox;

    final targetBox = context.findRenderObject() as RenderBox;
    targetSize = targetBox.size;
    targetOffset = targetBox.localToGlobal(
          Offset.zero,
          ancestor: navigatorBox,
        ) +
        Offset(0, targetSize.height);
    targetRect = targetBox.localToGlobal(
          Offset.zero,
          ancestor: navigatorBox,
        ) &
        targetSize;
  }

  _open = true;
  notifyListeners();

  final flyoutKey = GlobalKey();

  final result = await navigator.push<T>(PageRouteBuilder<T>(
    opaque: false,
    transitionDuration: transitionDuration,
    reverseTransitionDuration: transitionDuration,
    settings: settings,
    fullscreenDialog: true,
    pageBuilder: (context, animation, secondary) {
      transitionBuilder ??= (context, animation, placementMode, flyout) {
        switch (placementMode) {
          case FlyoutPlacementMode.bottomCenter:
          case FlyoutPlacementMode.bottomLeft:
          case FlyoutPlacementMode.bottomRight:
            return SlideTransition(
              position: Tween<Offset>(
                begin: const Offset(0, -0.05),
                end: const Offset(0, 0),
              ).animate(animation),
              child: flyout,
            );
          case FlyoutPlacementMode.topCenter:
          case FlyoutPlacementMode.topLeft:
          case FlyoutPlacementMode.topRight:
            return SlideTransition(
              position: Tween<Offset>(
                begin: const Offset(0, 0.05),
                end: const Offset(0, 0),
              ).animate(animation),
              child: flyout,
            );
          default:
            return flyout;
        }
      };

      return MenuInfoProvider(
        builder: (context, rootSize, menus, keys) {
          assert(menus.length == keys.length);

          final barrier = ColoredBox(
            color: barrierColor ?? Colors.black.withOpacity(0.3),
          );

          Widget box = Stack(children: [
            if (barrierRecognizer != null)
              Positioned.fill(
                child: Listener(
                  behavior: HitTestBehavior.opaque,
                  onPointerDown: (event) {
                    barrierRecognizer.addPointer(event);
                  },
                  child: barrier,
                ),
              )
            else if (barrierDismissible)
              Positioned.fill(
                child: GestureDetector(
                  behavior: HitTestBehavior.opaque,
                  onTap: barrierDismissible ? navigator.pop : null,
                  child: barrier,
                ),
              ),
            Positioned.fill(
              child: SafeArea(
                child: CustomSingleChildLayout(
                  delegate: _FlyoutPositionDelegate(
                    targetOffset: targetOffset,
                    targetSize: position == null ? targetSize : Size.zero,
                    autoModeConfiguration: autoModeConfiguration,
                    placementMode: placementMode,
                    defaultPreferred: position == null
                        ? FlyoutPlacementMode.topCenter
                        : FlyoutPlacementMode.bottomLeft,
                    margin: margin,
                    shouldConstrainToRootBounds: shouldConstrainToRootBounds,
                    forceAvailableSpace: forceAvailableSpace,
                  ),
                  child: Flyout(
                    rootFlyout: flyoutKey,
                    additionalOffset: additionalOffset,
                    margin: margin,
                    transitionDuration: transitionDuration!,
                    root: navigator,
                    builder: (context) {
                      final parentBox =
                          context.findAncestorRenderObjectOfType<
                              RenderCustomSingleChildLayoutBox>()!;
                      final delegate =
                          parentBox.delegate as _FlyoutPositionDelegate;

                      final realPlacementMode = delegate.autoPlacementMode ??
                          delegate.placementMode;
                      final flyout = Padding(
                        key: flyoutKey,
                        padding:
                            realPlacementMode._getAdditionalOffsetPosition(
                          position == null ? additionalOffset : 0.0,
                        ),
                        child: builder(context),
                      );

                      return transitionBuilder!(
                        context,
                        animation,
                        realPlacementMode,
                        flyout,
                      );
                    },
                  ),
                ),
              ),
            ),
            ...menus,
          ]);

          if (dismissOnPointerMoveAway) {
            box = MouseRegion(
              onHover: (hover) {
                if (flyoutKey.currentContext == null) return;

                final navigatorBox =
                    navigator.context.findRenderObject() as RenderBox;

                // the flyout box needs to be fetched at each [onHover] because the
                // flyout size may change (a MenuFlyout, for example)
                final flyoutBox =
                    flyoutKey.currentContext!.findRenderObject() as RenderBox;
                final flyoutRect = flyoutBox.localToGlobal(
                      Offset.zero,
                      ancestor: navigatorBox,
                    ) &
                    flyoutBox.size;
                final menusRects = keys.map((key) {
                  if (key.currentContext == null) return Rect.zero;

                  final menuBox =
                      key.currentContext!.findRenderObject() as RenderBox;
                  return menuBox.localToGlobal(
                        Offset.zero,
                        ancestor: navigatorBox,
                      ) &
                      menuBox.size;
                });

                if (!flyoutRect.contains(hover.position) &&
                    !targetRect.contains(hover.position) &&
                    !menusRects
                        .any((rect) => rect.contains(hover.position))) {
                  navigator.pop();
                }
              },
              child: box,
            );
          }

          if (dismissWithEsc) {
            box = Actions(
              actions: {DismissIntent: _DismissAction(navigator.pop)},
              child: FocusScope(
                autofocus: true,
                child: box,
              ),
            );
          }

          return FadeTransition(
            opacity: CurvedAnimation(
              curve: Curves.ease,
              parent: animation,
            ),
            child: box,
          );
        },
      );
    },
  ));

  _open = false;
  notifyListeners();

  return result;
}