// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/// @docImport 'context_menu_action.dart';
library;

import 'dart:math' as math;
import 'dart:ui' as ui;

import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart' show HapticFeedback;
import 'package:flutter/widgets.dart';

import 'colors.dart';
import 'localizations.dart';
import 'scrollbar.dart';

// The scale of the child at the time that the CupertinoContextMenu opens.
// This value was eyeballed from a physical device running iOS 13.1.2.
const double _kOpenScale = 1.15;

// The smallest possible scale of the child, used if opening the
// CupertinoContextMenu would cause it to go outside the safe area. This value
// was eyeballed from the Xcode iPhone simulator running iOS 16.1.
const double _kMinScaleFactor = 1.02;

// The ratio for the borderRadius of the context menu preview image. This value
// was eyeballed by overlapping the CupertinoContextMenu with a context menu
// from iOS 16.0 in the Xcode iPhone simulator.
const double _previewBorderRadiusRatio = 12.0;

// The duration of the transition used when a modal popup is shown. Eyeballed
// from a physical device running iOS 13.1.2.
const Duration _kModalPopupTransitionDuration = Duration(milliseconds: 335);

// The duration it takes for the CupertinoContextMenu to open.
// This value was eyeballed from the Xcode simulator running iOS 16.0.
const Duration _previewLongPressTimeout = Duration(milliseconds: 800);

// The total length of the combined animations until the menu is fully open.
final int _animationDuration =
    _previewLongPressTimeout.inMilliseconds + _kModalPopupTransitionDuration.inMilliseconds;

// The final box shadow for the opening child widget.
// This value was eyeballed from the Xcode simulator running iOS 16.0.
const List<BoxShadow> _endBoxShadow = <BoxShadow>[
  BoxShadow(color: Color(0x40000000), blurRadius: 10.0, spreadRadius: 0.5),
];

const Color _borderColor = CupertinoDynamicColor.withBrightness(
  color: Color(0xFFA9A9AF),
  darkColor: Color(0xFF57585A),
);

const Color _kBackgroundColor = CupertinoDynamicColor.withBrightness(
  color: Color(0xFFF1F1F1),
  darkColor: Color(0xFF212122),
);

typedef _DismissCallback = void Function(BuildContext context, double scale, double opacity);

/// A function that builds the child and handles the transition between the
/// default child and the preview when the CupertinoContextMenu is open.
typedef CupertinoContextMenuBuilder =
    Widget Function(BuildContext context, Animation<double> animation);

// Given a GlobalKey, return the Rect of the corresponding RenderBox's
// paintBounds in global coordinates.
Rect _getRect(GlobalKey globalKey) {
  assert(globalKey.currentContext != null);
  final renderBoxContainer = globalKey.currentContext!.findRenderObject()! as RenderBox;
  return Rect.fromPoints(
    renderBoxContainer.localToGlobal(renderBoxContainer.paintBounds.topLeft),
    renderBoxContainer.localToGlobal(renderBoxContainer.paintBounds.bottomRight),
  );
}

// The context menu arranges itself slightly differently based on the location
// on the screen of [CupertinoContextMenu.child] before the
// [CupertinoContextMenu] opens.
enum _ContextMenuLocation { center, left, right }

/// A full-screen modal route that opens when the [child] is long-pressed.
///
/// When open, the [CupertinoContextMenu] shows the child in a large full-screen
/// [Overlay] with a list of buttons specified by [actions]. The child/preview is
/// placed in an [Expanded] widget so that it will grow to fill the Overlay if
/// its size is unconstrained.
///
/// When closed, the [CupertinoContextMenu] displays the child as if the
/// [CupertinoContextMenu] were not there. Sizing and positioning is unaffected.
/// The menu can be closed like other [PopupRoute]s, such as by tapping the
/// background or by calling `Navigator.pop(context)`. Unlike [PopupRoute], it can
/// also be closed by swiping downwards.
///
/// {@tool dartpad}
/// This sample shows a very simple [CupertinoContextMenu] for the Flutter logo.
/// Long press on it to open.
///
/// ** See code in examples/api/lib/cupertino/context_menu/cupertino_context_menu.0.dart **
/// {@end-tool}
///
/// {@tool dartpad}
/// This sample shows a similar CupertinoContextMenu, this time using [builder]
/// to add a border radius to the widget.
///
/// ** See code in examples/api/lib/cupertino/context_menu/cupertino_context_menu.1.dart **
/// {@end-tool}
///
/// See also:
///
///  * <https://developer.apple.com/design/human-interface-guidelines/ios/controls/context-menus/>
class CupertinoContextMenu extends StatefulWidget {
  /// Create a context menu.
  ///
  /// The [actions] parameter cannot be empty.
  CupertinoContextMenu({
    super.key,
    required this.actions,
    required Widget this.child,
    this.enableHapticFeedback = false,
  }) : assert(actions.isNotEmpty),
       builder = ((BuildContext context, Animation<double> animation) => child);

  /// Creates a context menu with a custom [builder] controlling the widget.
  ///
  /// Use instead of the default constructor when it is needed to have a more
  /// custom animation.
  ///
  /// The [actions] parameter cannot be empty.
  CupertinoContextMenu.builder({
    super.key,
    required this.actions,
    required this.builder,
    this.enableHapticFeedback = false,
  }) : assert(actions.isNotEmpty),
       child = null;

  /// Exposes the default border radius for matching iOS 16.0 behavior. This
  /// value was eyeballed from the iOS simulator running iOS 16.0.
  ///
  /// {@tool snippet}
  ///
  /// Below is example code in order to match the default border radius for an
  /// iOS 16.0 open preview.
  ///
  /// ```dart
  /// CupertinoContextMenu.builder(
  ///   actions: <Widget>[
  ///     CupertinoContextMenuAction(
  ///       child: const Text('Action one'),
  ///       onPressed: () {},
  ///     ),
  ///   ],
  ///   builder:(BuildContext context, Animation<double> animation) {
  ///     final Animation<BorderRadius?> borderRadiusAnimation = BorderRadiusTween(
  ///       begin: BorderRadius.circular(0.0),
  ///       end: BorderRadius.circular(CupertinoContextMenu.kOpenBorderRadius),
  ///     ).animate(
  ///       CurvedAnimation(
  ///         parent: animation,
  ///         curve: Interval(
  ///           CupertinoContextMenu.animationOpensAt,
  ///           1.0,
  ///         ),
  ///       ),
  ///     );
  ///
  ///     final Animation<Decoration> boxDecorationAnimation = DecorationTween(
  ///       begin: const BoxDecoration(
  ///        boxShadow: <BoxShadow>[],
  ///       ),
  ///       end: const BoxDecoration(
  ///        boxShadow: CupertinoContextMenu.kEndBoxShadow,
  ///       ),
  ///      ).animate(
  ///        CurvedAnimation(
  ///         parent: animation,
  ///         curve: Interval(
  ///           0.0,
  ///           CupertinoContextMenu.animationOpensAt,
  ///         ),
  ///       )
  ///     );
  ///
  ///     return Container(
  ///       decoration:
  ///         animation.value < CupertinoContextMenu.animationOpensAt ? boxDecorationAnimation.value : null,
  ///       child: FittedBox(
  ///         fit: BoxFit.cover,
  ///         child: ClipRSuperellipse(
  ///           borderRadius: borderRadiusAnimation.value ?? BorderRadius.circular(0.0),
  ///           child: SizedBox(
  ///             height: 150,
  ///             width: 150,
  ///             child: Image.network('https://flutter.github.io/assets-for-api-docs/assets/widgets/owl-2.jpg'),
  ///           ),
  ///         ),
  ///       )
  ///     );
  ///   },
  /// )
  /// ```
  ///
  /// {@end-tool}
  static const double kOpenBorderRadius = _previewBorderRadiusRatio;

  /// Exposes the final box shadow of the opening animation of the child widget
  /// to match the default behavior of the native iOS widget. This value was
  /// eyeballed from the iOS simulator running iOS 16.0.
  static const List<BoxShadow> kEndBoxShadow = _endBoxShadow;

  /// The point at which the CupertinoContextMenu begins to animate
  /// into the open position.
  ///
  /// A value between 0.0 and 1.0 corresponding to a point in [builder]'s
  /// animation. When passing in an animation to [builder] the range before
  /// [animationOpensAt] will correspond to the animation when the widget is
  /// pressed and held, and the range after is the animation as the menu is
  /// fully opening. For an example, see the documentation for [builder].
  static final double animationOpensAt =
      _previewLongPressTimeout.inMilliseconds / _animationDuration;

  /// The background color of a [CupertinoContextMenuAction] and a
  /// [CupertinoContextMenu] sheet.
  static const Color kBackgroundColor = _kBackgroundColor;

  /// A function that returns a widget to be used alternatively from [child].
  ///
  /// The widget returned by the function will be shown at all times: when the
  /// [CupertinoContextMenu] is closed, when it is in the middle of opening,
  /// and when it is fully open. This will overwrite the default animation that
  /// matches the behavior of an iOS 16.0 context menu.
  ///
  /// This builder can be used instead of the child when the intended child has
  /// a property that would conflict with the default animation, such as a
  /// border radius or a shadow, or if a more custom animation is needed.
  ///
  /// In addition to the current [BuildContext], the function is also called
  /// with an [Animation]. The complete animation goes from 0 to 1 when
  /// the CupertinoContextMenu opens, and from 1 to 0 when it closes, and it can
  /// be used to animate the widget in sync with this opening and closing.
  ///
  /// The animation works in two stages. The first happens on press and hold of
  /// the widget from 0 to [animationOpensAt], and the second stage for when the
  /// widget fully opens up to the menu, from [animationOpensAt] to 1.
  ///
  /// {@tool snippet}
  ///
  /// Below is an example of using [builder] to show an image tile setup to be
  /// opened in the default way to match a native iOS 16.0 app. The behavior
  /// will match what will happen if the simple child image was passed as just
  /// the [child] parameter, instead of [builder]. This can be manipulated to
  /// add more customizability to the widget's animation.
  ///
  /// ```dart
  /// CupertinoContextMenu.builder(
  ///   actions: <Widget>[
  ///     CupertinoContextMenuAction(
  ///       child: const Text('Action one'),
  ///       onPressed: () {},
  ///     ),
  ///   ],
  ///   builder:(BuildContext context, Animation<double> animation) {
  ///     final Animation<BorderRadius?> borderRadiusAnimation = BorderRadiusTween(
  ///       begin: BorderRadius.circular(0.0),
  ///       end: BorderRadius.circular(CupertinoContextMenu.kOpenBorderRadius),
  ///     ).animate(
  ///       CurvedAnimation(
  ///         parent: animation,
  ///         curve: Interval(
  ///           CupertinoContextMenu.animationOpensAt,
  ///           1.0,
  ///         ),
  ///       ),
  ///      );
  ///
  ///     final Animation<Decoration> boxDecorationAnimation = DecorationTween(
  ///       begin: const BoxDecoration(
  ///        boxShadow: <BoxShadow>[],
  ///       ),
  ///       end: const BoxDecoration(
  ///        boxShadow: CupertinoContextMenu.kEndBoxShadow,
  ///       ),
  ///      ).animate(
  ///        CurvedAnimation(
  ///         parent: animation,
  ///         curve: Interval(
  ///           0.0,
  ///           CupertinoContextMenu.animationOpensAt,
  ///         ),
  ///       ),
  ///     );
  ///
  ///     return Container(
  ///       decoration:
  ///         animation.value < CupertinoContextMenu.animationOpensAt ? boxDecorationAnimation.value : null,
  ///       child: FittedBox(
  ///         fit: BoxFit.cover,
  ///         child: ClipRSuperellipse(
  ///           borderRadius: borderRadiusAnimation.value ?? BorderRadius.circular(0.0),
  ///           child: SizedBox(
  ///             height: 150,
  ///             width: 150,
  ///             child: Image.network('https://flutter.github.io/assets-for-api-docs/assets/widgets/owl-2.jpg'),
  ///           ),
  ///         ),
  ///       ),
  ///     );
  ///   },
  /// )
  /// ```
  ///
  /// {@end-tool}
  ///
  /// {@tool dartpad}
  /// Additionally below is an example of a real world use case for [builder].
  ///
  /// If a widget is passed to the [child] parameter with properties that
  /// conflict with the default animation, in this case the border radius,
  /// unwanted behaviors can arise. Here a boxed shadow will wrap the widget as
  /// it is expanded. To handle this, a more custom animation and widget can be
  /// passed to the builder, using values exposed by [CupertinoContextMenu],
  /// like [CupertinoContextMenu.kEndBoxShadow], to match the native iOS
  /// animation as close as desired.
  ///
  /// ** See code in examples/api/lib/cupertino/context_menu/cupertino_context_menu.1.dart **
  /// {@end-tool}
  final CupertinoContextMenuBuilder builder;

  // TODO(mitchgoodwin): deprecate [child] with builder refactor https://github.com/flutter/flutter/issues/116306

  /// The widget that can be "opened" with the [CupertinoContextMenu].
  ///
  /// When the [CupertinoContextMenu] is long-pressed, the menu will open and
  /// this widget will be moved to the new route and placed inside of an
  /// [Expanded] widget. This allows the child to resize to fit in its place in
  /// the new route, if it doesn't size itself.
  ///
  /// When the [CupertinoContextMenu] is "closed", this widget acts like a
  /// [Container], i.e. it does not constrain its child's size or affect its
  /// position.
  final Widget? child;

  /// The actions that are shown in the menu.
  ///
  /// These actions are typically [CupertinoContextMenuAction]s.
  ///
  /// This parameter must not be empty.
  final List<Widget> actions;

  /// If true, clicking on the [CupertinoContextMenuAction]s will
  /// produce haptic feedback.
  ///
  /// Uses [HapticFeedback.heavyImpact] when activated.
  /// Defaults to false.
  final bool enableHapticFeedback;

  @override
  State<CupertinoContextMenu> createState() => _CupertinoContextMenuState();
}

class _CupertinoContextMenuState extends State<CupertinoContextMenu> with TickerProviderStateMixin {
  final GlobalKey _childGlobalKey = GlobalKey();
  bool _childHidden = false;
  // Animates the child while it's opening.
  late AnimationController _openController;
  Rect? _decoyChildEndRect;
  late double _scaleFactor;
  OverlayEntry? _lastOverlayEntry;
  _ContextMenuRoute<void>? _route;
  final double _midpoint = CupertinoContextMenu.animationOpensAt / 2;
  late final TapGestureRecognizer _tapGestureRecognizer;

  @override
  void initState() {
    super.initState();
    _openController = AnimationController(
      duration: _previewLongPressTimeout,
      vsync: this,
      upperBound: CupertinoContextMenu.animationOpensAt,
    );
    _openController.addStatusListener(_onDecoyAnimationStatusChange);
    _tapGestureRecognizer = TapGestureRecognizer()
      ..onTapCancel = _onTapCancel
      ..onTapDown = _onTapDown
      ..onTapUp = _onTapUp
      ..onTap = _onTap;
  }

  void _listenerCallback() {
    if (_openController.status != AnimationStatus.reverse && _openController.value >= _midpoint) {
      if (widget.enableHapticFeedback) {
        HapticFeedback.heavyImpact();
      }
      _tapGestureRecognizer.resolve(GestureDisposition.accepted);
      _openController.removeListener(_listenerCallback);
    }
  }

  // Determine the _ContextMenuLocation based on the location of the original
  // child in the screen.
  //
  // The location of the original child is used to determine how to horizontally
  // align the content of the open CupertinoContextMenu. For example, if the
  // child is near the center of the screen, it will also appear in the center
  // of the screen when the menu is open, and the actions will be centered below
  // it.
  _ContextMenuLocation get _contextMenuLocation {
    final Rect childRect = _getRect(_childGlobalKey);
    final double screenWidth = MediaQuery.widthOf(context);

    final double center = screenWidth / 2;
    final bool centerDividesChild = childRect.left < center && childRect.right > center;
    final double distanceFromCenter = (center - childRect.center.dx).abs();
    if (centerDividesChild && distanceFromCenter <= childRect.width / 4) {
      return _ContextMenuLocation.center;
    }

    if (childRect.center.dx > center) {
      return _ContextMenuLocation.right;
    }

    return _ContextMenuLocation.left;
  }

  // Constrain the size of the expanded child so that it does not go outside the
  // safe area.
  //
  // See https://github.com/flutter/flutter/issues/122951.
  static double _getScaleFactor(Rect childRect, EdgeInsets padding, Size size) {
    final double leftMaxScale = 2 * (childRect.center.dx - padding.left) / childRect.width;
    final double topMaxScale = 2 * (childRect.center.dy - padding.top) / childRect.height;
    final double rightMaxScale =
        2 * (size.width - padding.right - childRect.center.dx) / childRect.width;
    final double bottomMaxScale =
        2 * (size.height - padding.bottom - childRect.center.dy) / childRect.height;
    final double minWidth = math.min(leftMaxScale, rightMaxScale);
    final double minHeight = math.min(topMaxScale, bottomMaxScale);

    // Return the smallest scale factor that keeps the child mostly onscreen.
    return clampDouble(math.min(minWidth, minHeight), _kMinScaleFactor, _kOpenScale);
  }

  /// The default preview builder if none is provided. It makes a rectangle
  /// around the child widget with rounded borders, matching the iOS 16 opened
  /// context menu eyeballed on the Xcode iOS simulator.
  static Widget _defaultPreviewBuilder(
    BuildContext context,
    Animation<double> animation,
    Widget child,
  ) {
    return FittedBox(
      fit: BoxFit.cover,
      child: ClipRSuperellipse(
        borderRadius: BorderRadius.circular(_previewBorderRadiusRatio * animation.value),
        child: child,
      ),
    );
  }

  // Push the new route and open the CupertinoContextMenu overlay.
  void _openContextMenu() {
    setState(() {
      _childHidden = true;
    });

    _route = _ContextMenuRoute<void>(
      actions: widget.actions,
      barrierLabel: CupertinoLocalizations.of(context).menuDismissLabel,
      filter: ui.ImageFilter.blur(sigmaX: 5.0, sigmaY: 5.0),
      contextMenuLocation: _contextMenuLocation,
      previousChildRect: _decoyChildEndRect!,
      scaleFactor: _scaleFactor,
      builder: (BuildContext context, Animation<double> animation) {
        if (widget.child == null) {
          final Animation<double> localAnimation = Tween<double>(
            begin: CupertinoContextMenu.animationOpensAt,
            end: 1,
          ).animate(animation);
          return widget.builder(context, localAnimation);
        }
        return _defaultPreviewBuilder(context, animation, widget.child!);
      },
    );
    Navigator.of(context, rootNavigator: true).push<void>(_route!);
    _route!.animation!.addStatusListener(_routeAnimationStatusListener);
  }

  void _removeContextMenuDecoy() {
    // Keep the decoy on the screen for one extra frame. We have to do this
    // because _ContextMenuRoute renders its first frame offscreen.
    // Otherwise there would be a visible flash when nothing is rendered for
    // one frame.
    SchedulerBinding.instance.addPostFrameCallback((Duration _) {
      if (mounted) {
        _closeContextMenu();
        _openController.reset();
      }
    }, debugLabel: 'removeContextMenuDecoy');
  }

  void _closeContextMenu() {
    _lastOverlayEntry?.remove();
    _lastOverlayEntry?.dispose();
    _lastOverlayEntry = null;
  }

  void _onDecoyAnimationStatusChange(AnimationStatus animationStatus) {
    switch (animationStatus) {
      case AnimationStatus.dismissed:
        if (_route == null) {
          setState(() {
            _childHidden = false;
          });
        }
        _closeContextMenu();
      case AnimationStatus.completed:
        _openContextMenu();
        _removeContextMenuDecoy();
      case AnimationStatus.forward:
      case AnimationStatus.reverse:
        if (!ModalRoute.of(context)!.isCurrent) {
          _removeContextMenuDecoy();
        }
        return;
    }
  }

  // Watch for when _ContextMenuRoute is closed and return to the state where
  // the CupertinoContextMenu just behaves as a Container.
  void _routeAnimationStatusListener(AnimationStatus status) {
    if (!status.isDismissed) {
      return;
    }
    if (mounted) {
      setState(() {
        _childHidden = false;
      });
    }
    _route!.animation!.removeStatusListener(_routeAnimationStatusListener);
    _route = null;
  }

  void _onTapCompleted() {
    _openController.removeListener(_listenerCallback);
    if (_openController.isAnimating && _openController.value < _midpoint) {
      _openController.reverse();
    }
  }

  void _onTap() {
    _onTapCompleted();
  }

  void _onTapCancel() {
    _onTapCompleted();
  }

  void _onTapUp(TapUpDetails details) {
    _onTapCompleted();
  }

  void _onTapDown(TapDownDetails details) {
    _openController.addListener(_listenerCallback);
    setState(() {
      _childHidden = true;
    });

    final Rect childRect = _getRect(_childGlobalKey);
    _scaleFactor = _getScaleFactor(
      childRect,
      MediaQuery.paddingOf(context),
      MediaQuery.sizeOf(context),
    );
    _decoyChildEndRect = Rect.fromCenter(
      center: childRect.center,
      width: childRect.width * _scaleFactor,
      height: childRect.height * _scaleFactor,
    );

    // Create a decoy child in an overlay directly on top of the original child.
    // TODO(justinmc): There is a known inconsistency with native here, due to
    // doing the bounce animation using a decoy in the top level Overlay. The
    // decoy will pop on top of the AppBar if the child is partially behind it,
    // such as a top item in a partially scrolled view. However, if we don't use
    // an overlay, then the decoy will appear behind its neighboring widget when
    // it expands. This may be solvable by adding a widget to Scaffold that's
    // underneath the AppBar.
    _lastOverlayEntry = OverlayEntry(
      builder: (BuildContext context) {
        return _DecoyChild(
          beginRect: childRect,
          controller: _openController,
          endRect: _decoyChildEndRect,
          builder: widget.builder,
          child: widget.child,
        );
      },
    );
    Overlay.of(context, rootOverlay: true, debugRequiredFor: widget).insert(_lastOverlayEntry!);
    _openController.forward();
  }

  @override
  Widget build(BuildContext context) {
    return MouseRegion(
      cursor: kIsWeb ? SystemMouseCursors.click : MouseCursor.defer,
      child: Listener(
        onPointerDown: _tapGestureRecognizer.addPointer,
        child: TickerMode(
          enabled: !_childHidden,
          child: Visibility.maintain(
            key: _childGlobalKey,
            visible: !_childHidden,
            child: widget.builder(context, _openController),
          ),
        ),
      ),
    );
  }

  @override
  void dispose() {
    _closeContextMenu();
    _tapGestureRecognizer.dispose();
    _openController.dispose();
    super.dispose();
  }
}

// A floating copy of the CupertinoContextMenu's child.
//
// When the child is pressed, but before the CupertinoContextMenu opens, it does
// an animation where it slowly grows. This is implemented by hiding the
// original child and placing _DecoyChild on top of it in an Overlay. The use of
// an Overlay allows the _DecoyChild to appear on top of siblings of the
// original child.
class _DecoyChild extends StatefulWidget {
  const _DecoyChild({
    this.beginRect,
    required this.controller,
    this.endRect,
    this.child,
    this.builder,
  });

  final Rect? beginRect;
  final AnimationController controller;
  final Rect? endRect;
  final Widget? child;
  final CupertinoContextMenuBuilder? builder;

  @override
  _DecoyChildState createState() => _DecoyChildState();
}

class _DecoyChildState extends State<_DecoyChild> with TickerProviderStateMixin {
  late Animation<Rect?> _rect;
  late Animation<Decoration> _boxDecoration;
  late final CurvedAnimation _boxDecorationCurvedAnimation;

  @override
  void initState() {
    super.initState();

    const beginPause = 1.0;
    const openAnimationLength = 5.0;
    const double totalOpenAnimationLength = beginPause + openAnimationLength;
    final double endPause =
        ((totalOpenAnimationLength * _animationDuration) /
            _previewLongPressTimeout.inMilliseconds) -
        totalOpenAnimationLength;

    // The timing on the animation was eyeballed from the Xcode iOS simulator
    // running iOS 16.0.
    // Because the animation no longer goes from 0.0 to 1.0, but to a number
    // depending on the ratio between the press animation time and the opening
    // animation time, a pause needs to be added to the end of the tween
    // sequence that completes that ratio. This is to allow the animation to
    // fully complete as expected without doing crazy math to the _kOpenScale
    // value. This change was necessary from the inclusion of the builder and
    // the complete animation value that it passes along.
    _rect = TweenSequence<Rect?>(<TweenSequenceItem<Rect?>>[
      TweenSequenceItem<Rect?>(
        tween: RectTween(
          begin: widget.beginRect,
          end: widget.beginRect,
        ).chain(CurveTween(curve: Curves.linear)),
        weight: beginPause,
      ),
      TweenSequenceItem<Rect?>(
        tween: RectTween(
          begin: widget.beginRect,
          end: widget.endRect,
        ).chain(CurveTween(curve: Curves.easeOutSine)),
        weight: openAnimationLength,
      ),
      TweenSequenceItem<Rect?>(
        tween: RectTween(
          begin: widget.endRect,
          end: widget.endRect,
        ).chain(CurveTween(curve: Curves.linear)),
        weight: endPause,
      ),
    ]).animate(widget.controller);

    _boxDecorationCurvedAnimation = CurvedAnimation(
      parent: widget.controller,
      curve: Interval(0.0, CupertinoContextMenu.animationOpensAt),
    );
    _boxDecoration = DecorationTween(
      begin: const BoxDecoration(boxShadow: <BoxShadow>[]),
      end: const BoxDecoration(boxShadow: _endBoxShadow),
    ).animate(_boxDecorationCurvedAnimation);
  }

  Widget _buildAnimation(BuildContext context, Widget? child) {
    return Positioned.fromRect(
      rect: _rect.value!,
      child: Container(decoration: _boxDecoration.value, child: widget.child),
    );
  }

  Widget _buildBuilder(BuildContext context, Widget? child) {
    return Positioned.fromRect(
      rect: _rect.value!,
      child: widget.builder!(context, widget.controller),
    );
  }

  @override
  void dispose() {
    _boxDecorationCurvedAnimation.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        AnimatedBuilder(
          builder: widget.child != null ? _buildAnimation : _buildBuilder,
          animation: widget.controller,
        ),
      ],
    );
  }
}

// The open CupertinoContextMenu modal.
class _ContextMenuRoute<T> extends PopupRoute<T> {
  // Build a _ContextMenuRoute.
  _ContextMenuRoute({
    required List<Widget> actions,
    required _ContextMenuLocation contextMenuLocation,
    this.barrierLabel,
    CupertinoContextMenuBuilder? builder,
    super.filter,
    required Rect previousChildRect,
    required double scaleFactor,
    super.settings,
  }) : assert(actions.isNotEmpty),
       _actions = actions,
       _builder = builder,
       _contextMenuLocation = contextMenuLocation,
       _previousChildRect = previousChildRect,
       _scaleFactor = scaleFactor;

  // Barrier color for a Cupertino modal barrier.
  static const Color _kModalBarrierColor = Color(0x6604040F);

  final List<Widget> _actions;
  final CupertinoContextMenuBuilder? _builder;
  final GlobalKey _childGlobalKey = GlobalKey();
  final _ContextMenuLocation _contextMenuLocation;
  bool _externalOffstage = false;
  bool _internalOffstage = false;
  final double _scaleFactor;
  Orientation? _lastOrientation;
  // The Rect of the child at the moment that the CupertinoContextMenu opens.
  final Rect _previousChildRect;
  double? _scale = 1.0;
  final GlobalKey _sheetGlobalKey = GlobalKey();

  static final CurveTween _curve = CurveTween(curve: Curves.easeOutBack);
  static final CurveTween _curveReverse = CurveTween(curve: Curves.easeInBack);
  static final RectTween _rectTween = RectTween();
  static final Animatable<Rect?> _rectAnimatable = _rectTween.chain(_curve);
  static final RectTween _rectTweenReverse = RectTween();
  static final Animatable<Rect?> _rectAnimatableReverse = _rectTweenReverse.chain(_curveReverse);
  static final RectTween _sheetRectTween = RectTween();
  final Animatable<Rect?> _sheetRectAnimatable = _sheetRectTween.chain(_curve);
  final Animatable<Rect?> _sheetRectAnimatableReverse = _sheetRectTween.chain(_curveReverse);
  static final Tween<double> _sheetScaleTween = Tween<double>();
  static final Animatable<double> _sheetScaleAnimatable = _sheetScaleTween.chain(_curve);
  static final Animatable<double> _sheetScaleAnimatableReverse = _sheetScaleTween.chain(
    _curveReverse,
  );
  final Tween<double> _opacityTween = Tween<double>(begin: 0.0, end: 1.0);
  late Animation<double> _sheetOpacity;

  @override
  final String? barrierLabel;

  @override
  Color get barrierColor => _kModalBarrierColor;

  @override
  bool get barrierDismissible => true;

  @override
  bool get semanticsDismissible => false;

  @override
  Duration get transitionDuration => _kModalPopupTransitionDuration;

  CurvedAnimation? _curvedAnimation;

  CurvedAnimation? _sheetOpacityCurvedAnimation;

  // Getting the RenderBox doesn't include the scale from the Transform.scale,
  // so it's manually accounted for here.
  static Rect _getScaledRect(GlobalKey globalKey, double scale) {
    final Rect childRect = _getRect(globalKey);
    final Size sizeScaled = childRect.size * scale;
    final offsetScaled = Offset(
      childRect.left + (childRect.size.width - sizeScaled.width) / 2,
      childRect.top + (childRect.size.height - sizeScaled.height) / 2,
    );
    return offsetScaled & sizeScaled;
  }

  // Get the alignment for the _ContextMenuSheet's Transform.scale based on the
  // contextMenuLocation and orientation.
  static AlignmentDirectional getSheetAlignment(
    _ContextMenuLocation contextMenuLocation,
    Orientation orientation,
  ) {
    return switch (contextMenuLocation) {
      _ContextMenuLocation.center when orientation == Orientation.landscape =>
        AlignmentDirectional.topStart,
      _ContextMenuLocation.center => AlignmentDirectional.topCenter,
      _ContextMenuLocation.right => AlignmentDirectional.topEnd,
      _ContextMenuLocation.left => AlignmentDirectional.topStart,
    };
  }

  // The place to start the sheetRect animation from.
  static Rect _getSheetRectBegin(
    Orientation? orientation,
    _ContextMenuLocation contextMenuLocation,
    Rect childRect,
    Rect sheetRect,
  ) {
    switch (contextMenuLocation) {
      case _ContextMenuLocation.center:
        final Offset target = orientation == Orientation.portrait
            ? childRect.bottomCenter
            : childRect.topCenter;
        final Offset centered = target - Offset(sheetRect.width / 2, 0.0);
        return centered & sheetRect.size;
      case _ContextMenuLocation.right:
        final Offset target = orientation == Orientation.portrait
            ? childRect.bottomRight
            : childRect.topRight;
        return (target - Offset(sheetRect.width, 0.0)) & sheetRect.size;
      case _ContextMenuLocation.left:
        final Offset target = orientation == Orientation.portrait
            ? childRect.bottomLeft
            : childRect.topLeft;
        return target & sheetRect.size;
    }
  }

  void _onDismiss(BuildContext context, double scale, double opacity) {
    _scale = scale;
    _opacityTween.end = opacity;
    _sheetOpacityCurvedAnimation = CurvedAnimation(
      parent: animation!,
      curve: const Interval(0.9, 1.0),
    );
    _sheetOpacity = _opacityTween.animate(_sheetOpacityCurvedAnimation!);
    Navigator.of(context).pop();
  }

  // Take measurements on the child and _ContextMenuSheet and update the
  // animation tweens to match.
  void _updateTweenRects() {
    final Rect childRect = _scale == null
        ? _getRect(_childGlobalKey)
        : _getScaledRect(_childGlobalKey, _scale!);
    _rectTween.begin = _previousChildRect;
    _rectTween.end = childRect;

    // When opening, the transition happens from the end of the child's bounce
    // animation to the final state. When closing, it goes from the final state
    // to the original position before the bounce.
    final childRectOriginal = Rect.fromCenter(
      center: _previousChildRect.center,
      width: _previousChildRect.width / _scaleFactor,
      height: _previousChildRect.height / _scaleFactor,
    );

    final Rect sheetRect = _getRect(_sheetGlobalKey);
    final Rect sheetRectBegin = _getSheetRectBegin(
      _lastOrientation,
      _contextMenuLocation,
      childRectOriginal,
      sheetRect,
    );
    _sheetRectTween.begin = sheetRectBegin;
    _sheetRectTween.end = sheetRect;
    _sheetScaleTween.begin = 0.0;
    _sheetScaleTween.end = _scale;

    _rectTweenReverse.begin = childRectOriginal;
    _rectTweenReverse.end = childRect;
  }

  void _setOffstageInternally() {
    super.offstage = _externalOffstage || _internalOffstage;
    // It's necessary to call changedInternalState to get the backdrop to
    // update.
    changedInternalState();
  }

  @override
  bool didPop(T? result) {
    _updateTweenRects();
    return super.didPop(result);
  }

  @override
  set offstage(bool value) {
    _externalOffstage = value;
    _setOffstageInternally();
  }

  @override
  TickerFuture didPush() {
    _internalOffstage = true;
    _setOffstageInternally();

    // Render one frame offstage in the final position so that we can take
    // measurements of its layout and then animate to them.
    SchedulerBinding.instance.addPostFrameCallback((Duration _) {
      _updateTweenRects();
      _internalOffstage = false;
      _setOffstageInternally();
    }, debugLabel: 'renderContextMenuRouteOffstage');
    return super.didPush();
  }

  @override
  Animation<double> createAnimation() {
    final Animation<double> animation = super.createAnimation();
    if (_curvedAnimation?.parent != animation) {
      _curvedAnimation?.dispose();
      _curvedAnimation = CurvedAnimation(parent: animation, curve: Curves.linear);
    }
    _sheetOpacity = _opacityTween.animate(_curvedAnimation!);
    return animation;
  }

  @override
  Widget buildPage(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
  ) {
    // This is usually used to build the "page", which is then passed to
    // buildTransitions as child, the idea being that buildTransitions will
    // animate the entire page into the scene. In the case of _ContextMenuRoute,
    // two individual pieces of the page are animated into the scene in
    // buildTransitions, and a SizedBox.shrink() is returned here.
    return const SizedBox.shrink();
  }

  @override
  Widget buildTransitions(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    Widget child,
  ) {
    return OrientationBuilder(
      builder: (BuildContext context, Orientation orientation) {
        _lastOrientation = orientation;

        // While the animation is running, render everything in a Stack so that
        // they're movable.
        if (!animation.isCompleted) {
          final reverse = animation.status == AnimationStatus.reverse;
          final Rect rect = reverse
              ? _rectAnimatableReverse.evaluate(animation)!
              : _rectAnimatable.evaluate(animation)!;
          final Rect sheetRect = reverse
              ? _sheetRectAnimatableReverse.evaluate(animation)!
              : _sheetRectAnimatable.evaluate(animation)!;
          final double sheetScale = reverse
              ? _sheetScaleAnimatableReverse.evaluate(animation)
              : _sheetScaleAnimatable.evaluate(animation);
          return Stack(
            children: <Widget>[
              Positioned.fromRect(
                rect: sheetRect,
                child: FadeTransition(
                  opacity: _sheetOpacity,
                  child: Transform.scale(
                    alignment: getSheetAlignment(_contextMenuLocation, orientation),
                    scale: sheetScale,
                    child: _ContextMenuSheet(
                      key: _sheetGlobalKey,
                      actions: _actions,
                      contextMenuLocation: _contextMenuLocation,
                      orientation: orientation,
                    ),
                  ),
                ),
              ),
              Positioned.fromRect(
                key: _childGlobalKey,
                rect: rect,
                child: _builder!(context, animation),
              ),
            ],
          );
        }

        // When the animation is done, just render everything in a static layout
        // in the final position.
        return _ContextMenuRouteStatic(
          actions: _actions,
          childGlobalKey: _childGlobalKey,
          contextMenuLocation: _contextMenuLocation,
          onDismiss: _onDismiss,
          orientation: orientation,
          sheetGlobalKey: _sheetGlobalKey,
          childRect: _previousChildRect,
          child: _builder!(context, animation),
        );
      },
    );
  }

  @override
  void dispose() {
    _curvedAnimation?.dispose();
    _sheetOpacityCurvedAnimation?.dispose();
    super.dispose();
  }
}

// The final state of the _ContextMenuRoute after animating in and before
// animating out.
class _ContextMenuRouteStatic extends StatefulWidget {
  const _ContextMenuRouteStatic({
    this.actions,
    required this.child,
    this.childGlobalKey,
    required this.contextMenuLocation,
    this.onDismiss,
    required this.orientation,
    this.sheetGlobalKey,
    required this.childRect,
  });

  final List<Widget>? actions;
  final Widget child;
  final GlobalKey? childGlobalKey;
  final _ContextMenuLocation contextMenuLocation;
  final _DismissCallback? onDismiss;
  final Orientation orientation;
  final GlobalKey? sheetGlobalKey;
  final Rect childRect;

  @override
  _ContextMenuRouteStaticState createState() => _ContextMenuRouteStaticState();
}

class _ContextMenuRouteStaticState extends State<_ContextMenuRouteStatic>
    with TickerProviderStateMixin {
  // The child is scaled down as it is dragged down until it hits this minimum
  // value.
  static const double _kMinScale = 0.8;
  // The CupertinoContextMenuSheet disappears at this scale.
  static const double _kSheetScaleThreshold = 0.9;
  static const double _kPadding = 20.0;
  static const double _kDamping = 400.0;
  static const Duration _kMoveControllerDuration = Duration(milliseconds: 600);

  late Offset _dragOffset;
  double _lastScale = 1.0;
  late final AnimationController _moveController;
  late final CurvedAnimation _moveCurvedAnimation;
  late final AnimationController _sheetController;
  late final CurvedAnimation _sheetCurvedAnimation;
  late Animation<Offset> _moveAnimation;
  late Animation<double> _sheetScaleAnimation;
  late Animation<double> _sheetOpacityAnimation;

  // The scale of the child changes as a function of the distance it is dragged.
  static double _getScale(Orientation orientation, double maxDragDistance, double dy) {
    final double dyDirectional = dy <= 0.0 ? dy : -dy;
    return math.max(_kMinScale, (maxDragDistance + dyDirectional) / maxDragDistance);
  }

  void _onPanStart(DragStartDetails details) {
    _moveController.value = 1.0;
    _setDragOffset(Offset.zero);
  }

  void _onPanUpdate(DragUpdateDetails details) {
    _setDragOffset(_dragOffset + details.delta);
  }

  void _onPanEnd(DragEndDetails details) {
    // If flung, animate a bit before handling the potential dismiss.
    if (details.velocity.pixelsPerSecond.dy.abs() >= kMinFlingVelocity) {
      final bool flingIsAway = details.velocity.pixelsPerSecond.dy > 0;
      final double finalPosition = flingIsAway ? _moveAnimation.value.dy + 100.0 : 0.0;

      if (flingIsAway && _sheetController.status != AnimationStatus.forward) {
        _sheetController.forward();
      } else if (!flingIsAway && _sheetController.status != AnimationStatus.reverse) {
        _sheetController.reverse();
      }

      _moveAnimation = Tween<Offset>(
        begin: Offset(0.0, _moveAnimation.value.dy),
        end: Offset(0.0, finalPosition),
      ).animate(_moveController);
      _moveController.reset();
      _moveController.duration = const Duration(milliseconds: 64);
      _moveController.forward();
      _moveController.addStatusListener(_flingStatusListener);
      return;
    }

    // Dismiss if the drag is enough to scale down all the way.
    if (_lastScale == _kMinScale) {
      widget.onDismiss!(context, _lastScale, _sheetOpacityAnimation.value);
      return;
    }

    // Otherwise animate back home.
    _moveController.addListener(_moveListener);
    _moveController.reverse();
  }

  void _moveListener() {
    // When the scale passes the threshold, animate the sheet back in.
    if (_lastScale > _kSheetScaleThreshold) {
      _moveController.removeListener(_moveListener);
      if (!_sheetController.isDismissed) {
        _sheetController.reverse();
      }
    }
  }

  void _flingStatusListener(AnimationStatus status) {
    if (!status.isCompleted) {
      return;
    }

    // Reset the duration back to its original value.
    _moveController.duration = _kMoveControllerDuration;

    _moveController.removeStatusListener(_flingStatusListener);
    // If it was a fling back to the start, it has reset itself, and it should
    // not be dismissed.
    if (_moveAnimation.value.dy == 0.0) {
      return;
    }
    widget.onDismiss!(context, _lastScale, _sheetOpacityAnimation.value);
  }

  void _setDragOffset(Offset dragOffset) {
    // Allow horizontal and negative vertical movement, but damp it.
    final double endX = _kPadding * dragOffset.dx / _kDamping;
    final double endY = dragOffset.dy >= 0.0
        ? dragOffset.dy
        : _kPadding * dragOffset.dy / _kDamping;
    setState(() {
      _dragOffset = dragOffset;
      _moveAnimation = Tween<Offset>(
        begin: Offset.zero,
        end: Offset(clampDouble(endX, -_kPadding, _kPadding), endY),
      ).animate(_moveCurvedAnimation);

      // Fade the _ContextMenuSheet out or in, if needed.
      if (_lastScale <= _kSheetScaleThreshold &&
          _sheetController.status != AnimationStatus.forward &&
          _sheetScaleAnimation.value != 0.0) {
        _sheetController.forward();
      } else if (_lastScale > _kSheetScaleThreshold &&
          _sheetController.status != AnimationStatus.reverse &&
          _sheetScaleAnimation.value != 1.0) {
        _sheetController.reverse();
      }
    });
  }

  // The order and alignment of the _ContextMenuSheet and the child depend on
  // both the orientation of the screen as well as the position on the screen of
  // the original child.
  Widget _getChild(Orientation orientation, _ContextMenuLocation contextMenuLocation) {
    final Size screenSize = MediaQuery.sizeOf(context);
    final EdgeInsets padding = MediaQuery.paddingOf(context);
    final screenBounds = Rect.fromLTWH(
      0,
      0,
      screenSize.width - padding.left - padding.right,
      screenSize.height - padding.top - padding.bottom,
    );

    final Widget sheet = AnimatedBuilder(
      animation: _sheetController,
      builder: _buildSheetAnimation,
      child: _ContextMenuSheet(
        key: widget.sheetGlobalKey,
        actions: widget.actions!,
        contextMenuLocation: widget.contextMenuLocation,
        orientation: widget.orientation,
      ),
    );
    final Widget child = _ContextMenuAlignedChildren(
      targetRect: widget.childRect,
      screenBounds: screenBounds,
      sheet: sheet,
      contextMenuLocation: contextMenuLocation,
      orientation: widget.orientation,
      child: AnimatedBuilder(
        animation: _moveController,
        builder: _buildChildAnimation,
        child: widget.child,
      ),
    );

    return child;
  }

  // Build the animation for the _ContextMenuSheet.
  Widget _buildSheetAnimation(BuildContext context, Widget? child) {
    return Transform.scale(
      alignment: _ContextMenuRoute.getSheetAlignment(
        widget.contextMenuLocation,
        widget.orientation,
      ),
      scale: _sheetScaleAnimation.value,
      child: FadeTransition(opacity: _sheetOpacityAnimation, child: child),
    );
  }

  // Build the animation for the child.
  Widget _buildChildAnimation(BuildContext context, Widget? child) {
    _lastScale = _getScale(
      widget.orientation,
      MediaQuery.heightOf(context),
      _moveAnimation.value.dy,
    );
    return Transform.scale(key: widget.childGlobalKey, scale: _lastScale, child: child);
  }

  // Build the animation for the overall draggable dismissible content.
  Widget _buildAnimation(BuildContext context, Widget? child) {
    return Transform.translate(offset: _moveAnimation.value, child: child);
  }

  @override
  void initState() {
    super.initState();
    _moveController = AnimationController(
      duration: _kMoveControllerDuration,
      value: 1.0,
      vsync: this,
    );
    _moveCurvedAnimation = CurvedAnimation(parent: _moveController, curve: Curves.elasticIn);
    _sheetController = AnimationController(
      duration: const Duration(milliseconds: 100),
      reverseDuration: const Duration(milliseconds: 300),
      vsync: this,
    );
    _sheetCurvedAnimation = CurvedAnimation(
      parent: _sheetController,
      curve: Curves.linear,
      reverseCurve: Curves.easeInBack,
    );
    _sheetScaleAnimation = Tween<double>(begin: 1.0, end: 0.0).animate(_sheetCurvedAnimation);
    _sheetOpacityAnimation = Tween<double>(begin: 1.0, end: 0.0).animate(_sheetController);
    _setDragOffset(Offset.zero);
  }

  @override
  void dispose() {
    _moveController.dispose();
    _moveCurvedAnimation.dispose();
    _sheetController.dispose();
    _sheetCurvedAnimation.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final Widget child = _getChild(widget.orientation, widget.contextMenuLocation);

    return SafeArea(
      child: Align(
        alignment: Alignment.topLeft,
        child: GestureDetector(
          onPanEnd: _onPanEnd,
          onPanStart: _onPanStart,
          onPanUpdate: _onPanUpdate,
          child: AnimatedBuilder(
            animation: _moveController,
            builder: _buildAnimation,
            child: child,
          ),
        ),
      ),
    );
  }
}

// The menu that displays when CupertinoContextMenu is open. It consists of a
// list of actions that are typically CupertinoContextMenuActions.
class _ContextMenuSheet extends StatefulWidget {
  _ContextMenuSheet({
    super.key,
    required this.actions,
    required this.contextMenuLocation,
    required this.orientation,
  }) : assert(actions.isNotEmpty);

  final List<Widget> actions;
  final _ContextMenuLocation contextMenuLocation;
  final Orientation orientation;

  @override
  State<_ContextMenuSheet> createState() => _ContextMenuSheetState();
}

class _ContextMenuSheetState extends State<_ContextMenuSheet> {
  late final ScrollController _controller;
  static const double _kMenuWidth = 250.0;
  // Eyeballed on a context menu on an iOS 15 simulator running iOS 17.5.
  static const double _kScrollbarMainAxisMargin = 13.0;

  @override
  void initState() {
    super.initState();
    // Link the scrollbar to the scroll view by providing both the same scroll
    // controller. Using SingleChildScrollview.primary might conflict with users
    // already using the PrimaryScrollController.
    _controller = ScrollController();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: _kMenuWidth,
      child: IntrinsicHeight(
        child: ClipRSuperellipse(
          borderRadius: const BorderRadius.all(Radius.circular(13.0)),
          child: ColoredBox(
            color: CupertinoDynamicColor.resolve(CupertinoContextMenu.kBackgroundColor, context),
            child: ScrollConfiguration(
              behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
              child: CupertinoScrollbar(
                mainAxisMargin: _kScrollbarMainAxisMargin,
                controller: _controller,
                child: SingleChildScrollView(
                  controller: _controller,
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.stretch,
                    children: <Widget>[
                      widget.actions.first,
                      for (final Widget action in widget.actions.skip(1))
                        DecoratedBox(
                          decoration: BoxDecoration(
                            border: Border(
                              top: BorderSide(
                                color: CupertinoDynamicColor.resolve(_borderColor, context),
                                width: 0.4,
                              ),
                            ),
                          ),
                          position: DecorationPosition.foreground,
                          child: action,
                        ),
                    ],
                  ),
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

enum _ContextMenuChild { child, menuSheet }

class _ContextMenuAlignedChildren extends StatelessWidget {
  const _ContextMenuAlignedChildren({
    required this.targetRect,
    required this.screenBounds,
    required this.child,
    required this.sheet,
    required this.orientation,
    required this.contextMenuLocation,
  });
  final Rect targetRect;
  final Rect screenBounds;
  final Widget child;
  final Widget sheet;
  final Orientation orientation;
  final _ContextMenuLocation contextMenuLocation;

  @override
  Widget build(BuildContext context) {
    return CustomMultiChildLayout(
      delegate: _ContextMenuAlignedChildrenDelegate(
        targetRect: targetRect,
        screenBounds: screenBounds,
        orientation: orientation,
        contextMenuLocation: contextMenuLocation,
      ),
      children: <Widget>[
        LayoutId(id: _ContextMenuChild.child, child: child),
        LayoutId(id: _ContextMenuChild.menuSheet, child: sheet),
      ],
    );
  }
}

class _ContextMenuAlignedChildrenDelegate extends MultiChildLayoutDelegate {
  _ContextMenuAlignedChildrenDelegate({
    required this.targetRect,
    required this.screenBounds,
    required this.orientation,
    required this.contextMenuLocation,
  });
  final Rect targetRect;
  final Rect screenBounds;
  final Orientation orientation;
  final _ContextMenuLocation contextMenuLocation;

  @override
  void performLayout(Size size) {
    final constraints = BoxConstraints.loose(size);

    final double availableHeightForChild =
        screenBounds.height - _ContextMenuRouteStaticState._kPadding;
    final double availableWidth = screenBounds.width - _ContextMenuRouteStaticState._kPadding * 2;
    final double availableWidthForChild = switch (orientation) {
      Orientation.portrait => availableWidth,
      Orientation.landscape => availableWidth - _ContextMenuSheetState._kMenuWidth,
    };
    assert(availableWidthForChild >= 0.0);
    assert(availableHeightForChild >= 0.0);

    final Size childSize = layoutChild(
      _ContextMenuChild.child,
      constraints.copyWith(maxHeight: availableHeightForChild, maxWidth: availableWidthForChild),
    );

    // In portrait orientation, the child is atop the menu, while in landscape
    // orientation, the child is beside the menu.
    final double availableHeightForMenu = switch (orientation) {
      Orientation.portrait =>
        availableHeightForChild - (childSize.height + _ContextMenuRouteStaticState._kPadding),
      Orientation.landscape => availableHeightForChild,
    };

    final Size menuSize = layoutChild(
      _ContextMenuChild.menuSheet,
      constraints.copyWith(maxHeight: availableHeightForMenu),
    );

    final double initialChildLeft;
    final double initialChildTop;
    final double maxClampedLeft;
    final double maxClampedTop;
    final Offset secondChildOffset;
    final bool menuBeforeChild;
    switch (orientation) {
      case Orientation.portrait:
        menuBeforeChild = false;
        final double totalHeight =
            childSize.height + menuSize.height + _ContextMenuRouteStaticState._kPadding;
        final double totalWidth = childSize.width + _ContextMenuRouteStaticState._kPadding;
        initialChildLeft = targetRect.center.dx - childSize.width / 2;
        initialChildTop = targetRect.center.dy - childSize.height;
        final double secondChildDx = switch (contextMenuLocation) {
          _ContextMenuLocation.center => childSize.width / 2 - menuSize.width / 2,
          _ContextMenuLocation.left => 0.0,
          _ContextMenuLocation.right => childSize.width - menuSize.width,
        };
        secondChildOffset = Offset(
          secondChildDx,
          childSize.height + _ContextMenuRouteStaticState._kPadding,
        );
        maxClampedLeft = screenBounds.right - totalWidth;
        maxClampedTop = screenBounds.bottom - totalHeight;
      case Orientation.landscape:
        menuBeforeChild = contextMenuLocation == _ContextMenuLocation.right;
        final double totalWidth =
            childSize.width + menuSize.width + _ContextMenuRouteStaticState._kPadding;
        initialChildLeft = screenBounds.center.dx - totalWidth / 2;
        initialChildTop = screenBounds.center.dy - math.max(childSize.height, menuSize.height) / 2;
        final double secondChildDx = menuBeforeChild ? menuSize.width : childSize.width;
        secondChildOffset = Offset(secondChildDx + _ContextMenuRouteStaticState._kPadding, 0.0);
        maxClampedLeft = screenBounds.right - totalWidth;
        maxClampedTop = screenBounds.bottom;
    }

    // Clamp the position to ensure it stays within screen bounds.
    final double clampedLeft = clampDouble(
      initialChildLeft,
      screenBounds.left + _ContextMenuRouteStaticState._kPadding,
      maxClampedLeft,
    );
    final double clampedTop = clampDouble(
      initialChildTop,
      screenBounds.top + _ContextMenuRouteStaticState._kPadding,
      maxClampedTop,
    );
    final firstPosition = Offset(clampedLeft, clampedTop);
    final Offset secondPosition = firstPosition + secondChildOffset;

    positionChild(_ContextMenuChild.child, menuBeforeChild ? secondPosition : firstPosition);
    positionChild(_ContextMenuChild.menuSheet, menuBeforeChild ? firstPosition : secondPosition);
  }

  @override
  bool shouldRelayout(_ContextMenuAlignedChildrenDelegate oldDelegate) {
    return oldDelegate.targetRect != targetRect ||
        oldDelegate.screenBounds != screenBounds ||
        oldDelegate.orientation != orientation ||
        oldDelegate.contextMenuLocation != contextMenuLocation;
  }
}
