computeMenuLayout static method
({ArrowConfig arrowConfig, Offset followerOffset, Rect panelRect, Offset pointerOffset})
computeMenuLayout({
- required BuildContext context,
- required Offset globalPosition,
- required double targetWidth,
- required int buttonCount,
- required Size overlaySize,
- required bool followAnchor,
- GlobalKey<
State< ? childKey,StatefulWidget> >
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
);
}