computeMenuLayout static method

({ArrowConfig arrowConfig, Offset followerOffset, Rect panelRect, Offset pointerOffset}) computeMenuLayout({
  1. required BuildContext context,
  2. required Offset globalPosition,
  3. required double targetWidth,
  4. required int buttonCount,
  5. required Size overlaySize,
  6. required bool followAnchor,
  7. GlobalKey<State<StatefulWidget>>? childKey,
})

Implementation

static ({
  Rect panelRect,
  ArrowConfig arrowConfig,
  Offset pointerOffset,
  Offset followerOffset
}) computeMenuLayout({
  required BuildContext context,
  required Offset globalPosition,
  required double targetWidth,
  required int buttonCount,
  required Size overlaySize,
  required bool followAnchor,
  GlobalKey? childKey,
}) {
  const Offset pointerDisplacement = Offset(3, 4);
  const double panelHorizontalGap = 10;
  const double panelVerticalGap = 8;
  const double buttonHeight = kButtonHeight;

  final overlayBox =
      Overlay.of(context).context.findRenderObject() as RenderBox;

  double panelHeight = (buttonCount * buttonHeight) + 12.0; // padding
  panelHeight = panelHeight.clamp(48.0, overlaySize.height).toDouble();

  // When followAnchor is true, compute positions relative to child widget
  Offset followerOffset = Offset.zero;
  Rect panelRect;
  Offset pointerOffset;
  Offset anchorLocal;

  if (followAnchor && childKey != null) {
    final ctx = childKey.currentContext;
    if (ctx != null) {
      final childBox = ctx.findRenderObject() as RenderBox?;
      if (childBox != null && childBox.attached) {
        // Get child's position and size
        final childTopLeftGlobal = childBox.localToGlobal(Offset.zero);
        final childTopLeftLocal =
            overlayBox.globalToLocal(childTopLeftGlobal);

        // Get click position relative to child
        final clickInChild = childBox.globalToLocal(globalPosition);

        // Calculate where to place menu relative to click, with displacement
        final displacedClickInChild = clickInChild + pointerDisplacement;

        // Determine if menu can fit to the right of click position
        final menuRightEdge = childTopLeftLocal.dx +
            displacedClickInChild.dx +
            panelHorizontalGap +
            targetWidth;

        final bool canPlaceToRight = menuRightEdge <= overlaySize.width;

        // Calculate follower offset (relative to child's top-left)
        double offsetX;
        double offsetY;

        if (canPlaceToRight) {
          offsetX = displacedClickInChild.dx + panelHorizontalGap;
        } else {
          offsetX =
              displacedClickInChild.dx - targetWidth - panelHorizontalGap;
        }

        offsetY = displacedClickInChild.dy - panelVerticalGap;

        // Clamp to keep menu on screen
        offsetX = offsetX
            .clamp(-childTopLeftLocal.dx,
                overlaySize.width - childTopLeftLocal.dx - targetWidth)
            .toDouble();
        offsetY = offsetY
            .clamp(-childTopLeftLocal.dy,
                overlaySize.height - childTopLeftLocal.dy - panelHeight)
            .toDouble();

        followerOffset = Offset(offsetX, offsetY);

        // Panel rect for follower mode: positioned at origin since follower handles positioning
        // But we still need the dimensions and a reference point for arrow calculation
        panelRect = Rect.fromLTWH(
          0,
          0,
          targetWidth,
          panelHeight,
        );

        // Pointer offset in overlay coordinates (for arrow calculation relative to panel at origin)
        pointerOffset = overlayBox.globalToLocal(globalPosition);
        // Adjust pointer to be relative to where the panel actually is (child + followerOffset)
        pointerOffset = Offset(
          pointerOffset.dx - childTopLeftLocal.dx - offsetX,
          pointerOffset.dy - childTopLeftLocal.dy - offsetY,
        );
        anchorLocal = pointerOffset;
      } else {
        // Fallback if child not available
        final Offset originalLocal = overlayBox.globalToLocal(globalPosition);
        final Offset displacedLocal = originalLocal + pointerDisplacement;
        anchorLocal = Offset(
          displacedLocal.dx.clamp(0.0, overlaySize.width).toDouble(),
          displacedLocal.dy.clamp(0.0, overlaySize.height).toDouble(),
        );
        pointerOffset = Offset(
          originalLocal.dx.clamp(0.0, overlaySize.width).toDouble(),
          originalLocal.dy.clamp(0.0, overlaySize.height).toDouble(),
        );

        final bool canPlaceToRight =
            anchorLocal.dx + panelHorizontalGap + targetWidth <=
                overlaySize.width;
        double panelLeft = canPlaceToRight
            ? anchorLocal.dx + panelHorizontalGap
            : anchorLocal.dx - panelHorizontalGap - targetWidth;
        panelLeft = panelLeft
            .clamp(0.0, math.max(0.0, overlaySize.width - targetWidth))
            .toDouble();
        double panelTop = anchorLocal.dy - panelVerticalGap;
        panelTop = panelTop
            .clamp(0.0, math.max(0.0, overlaySize.height - panelHeight))
            .toDouble();
        panelRect =
            Rect.fromLTWH(panelLeft, panelTop, targetWidth, panelHeight);
      }
    } else {
      // Fallback if context not available
      final Offset originalLocal = overlayBox.globalToLocal(globalPosition);
      final Offset displacedLocal = originalLocal + pointerDisplacement;
      anchorLocal = Offset(
        displacedLocal.dx.clamp(0.0, overlaySize.width).toDouble(),
        displacedLocal.dy.clamp(0.0, overlaySize.height).toDouble(),
      );
      pointerOffset = Offset(
        originalLocal.dx.clamp(0.0, overlaySize.width).toDouble(),
        originalLocal.dy.clamp(0.0, overlaySize.height).toDouble(),
      );

      final bool canPlaceToRight =
          anchorLocal.dx + panelHorizontalGap + targetWidth <=
              overlaySize.width;
      double panelLeft = canPlaceToRight
          ? anchorLocal.dx + panelHorizontalGap
          : anchorLocal.dx - panelHorizontalGap - targetWidth;
      panelLeft = panelLeft
          .clamp(0.0, math.max(0.0, overlaySize.width - targetWidth))
          .toDouble();
      double panelTop = anchorLocal.dy - panelVerticalGap;
      panelTop = panelTop
          .clamp(0.0, math.max(0.0, overlaySize.height - panelHeight))
          .toDouble();
      panelRect =
          Rect.fromLTWH(panelLeft, panelTop, targetWidth, panelHeight);
    }
  } else {
    // Original behavior when followAnchor is false
    final Offset originalLocal = overlayBox.globalToLocal(globalPosition);
    final Offset displacedLocal = originalLocal + pointerDisplacement;

    anchorLocal = Offset(
      displacedLocal.dx.clamp(0.0, overlaySize.width).toDouble(),
      displacedLocal.dy.clamp(0.0, overlaySize.height).toDouble(),
    );

    pointerOffset = Offset(
      originalLocal.dx.clamp(0.0, overlaySize.width).toDouble(),
      originalLocal.dy.clamp(0.0, overlaySize.height).toDouble(),
    );

    final bool canPlaceToRight =
        anchorLocal.dx + panelHorizontalGap + targetWidth <=
            overlaySize.width;
    double panelLeft = canPlaceToRight
        ? anchorLocal.dx + panelHorizontalGap
        : anchorLocal.dx - panelHorizontalGap - targetWidth;
    panelLeft = panelLeft
        .clamp(0.0, math.max(0.0, overlaySize.width - targetWidth))
        .toDouble();

    double panelTop = anchorLocal.dy - panelVerticalGap;
    panelTop = panelTop
        .clamp(0.0, math.max(0.0, overlaySize.height - panelHeight))
        .toDouble();

    panelRect = Rect.fromLTWH(panelLeft, panelTop, targetWidth, panelHeight);
  }

  final double panelBottom = panelRect.top + panelRect.height;
  final double panelCenterY = panelRect.top + (panelRect.height / 2);
  final bool pointerBelowPanel = pointerOffset.dy >= panelBottom;
  final bool pointerAbovePanel = pointerOffset.dy <= panelRect.top;
  final bool useBottomCorner = pointerBelowPanel
      ? true
      : (pointerAbovePanel ? false : pointerOffset.dy >= panelCenterY);

  final bool canPlaceToRight =
      (panelRect.left > anchorLocal.dx - targetWidth);
  final ArrowCorner arrowCorner;
  if (canPlaceToRight) {
    arrowCorner =
        useBottomCorner ? ArrowCorner.bottomLeft : ArrowCorner.topLeft;
  } else {
    arrowCorner =
        useBottomCorner ? ArrowCorner.bottomRight : ArrowCorner.topRight;
  }

  final arrowConfig = ArrowConfig(
    corner: arrowCorner,
    baseWidth: 10,
    cornerRadius: 4,
    tipGap: 2,
    maxLength: 2,
    shape: ArrowShape.smallTriangle,
    tipRoundness: 5,
  );

  return (
    panelRect: panelRect,
    arrowConfig: arrowConfig,
    pointerOffset: pointerOffset,
    followerOffset: followerOffset
  );
}