Skip to content

Instantly share code, notes, and snippets.

@rubywai
Last active June 4, 2025 03:58
Show Gist options
  • Save rubywai/44d0fc3a42a84a4c16f7a09cc6501eab to your computer and use it in GitHub Desktop.
Save rubywai/44d0fc3a42a84a4c16f7a09cc6501eab to your computer and use it in GitHub Desktop.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class CustomDialogLayout extends SingleChildLayoutDelegate {
CustomDialogLayout({
required this.anchorRect,
required this.textDirection,
required this.alignment,
required this.alignmentOffset,
required this.menuPadding,
required this.avoidBounds,
required this.dialogSize,
this.orientation = Axis.vertical,
this.parentOrientation = Axis.horizontal,
});
// Rectangle of underlying button, relative to the overlay's dimensions.
final Rect anchorRect;
// Whether to prefer going to the left or to the right.
final TextDirection textDirection;
// The alignment to use when finding the ideal location for the menu.
final AlignmentGeometry alignment;
// The offset from the alignment position to find the ideal location for the
// menu.
final Offset alignmentOffset;
// The padding on the inside of the menu, so it can be accounted for when
// positioning.
final EdgeInsetsGeometry menuPadding;
// List of rectangles that we should avoid overlapping. Unusable screen area.
final Set<Rect> avoidBounds;
// The orientation of this menu
Axis orientation;
// The orientation of this menu's parent.
Axis parentOrientation;
final Size dialogSize;
@override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
// The menu can be at most the size of the overlay minus _kMenuViewPadding
// pixels in each direction.
return BoxConstraints.loose(constraints.biggest).deflate(
const EdgeInsets.all(0),
);
}
@override
Offset getPositionForChild(Size size, Size childSize) {
// size: The size of the overlay.
// childSize: The size of the menu, when fully open, as determined by
// getConstraintsForChild.
final Rect overlayRect = Offset.zero & size;
double x;
double y;
Offset desiredPosition =
alignment.resolve(textDirection).withinRect(anchorRect);
final Offset directionalOffset;
if (alignment is AlignmentDirectional) {
switch (textDirection) {
case TextDirection.rtl:
directionalOffset = Offset(-alignmentOffset.dx, alignmentOffset.dy);
case TextDirection.ltr:
directionalOffset = alignmentOffset;
}
} else {
directionalOffset = alignmentOffset;
}
desiredPosition += directionalOffset;
x = desiredPosition.dx;
y = desiredPosition.dy;
if (textDirection == TextDirection.rtl) {
x -= childSize.width; // Use childSize instead of dialogSize
} else if (alignment.resolve(textDirection).x == 1) {
x -= childSize.width; // Use childSize instead of dialogSize
}
final Iterable<Rect> subScreens =
DisplayFeatureSubScreen.subScreensInBounds(overlayRect, avoidBounds);
final Rect allowedRect = _closestScreen(subScreens, anchorRect.center);
bool offLeftSide(double x) => x < allowedRect.left;
bool offRightSide(double x) =>
x + childSize.width > allowedRect.right; // Use childSize
bool offTop(double y) => y < allowedRect.top;
bool offBottom(double y) =>
y + childSize.height > allowedRect.bottom; // Use childSize
// Avoid going outside an area defined as the rectangle offset from the
// edge of the screen by the button padding. If the menu is off of the screen,
// move the menu to the other side of the button first, and then if it
// doesn't fit there, then just move it over as much as needed to make it
// fit.
if (childSize.width >= allowedRect.width) {
// Use childSize
// It just doesn't fit, so put as much on the screen as possible.
x = allowedRect.left;
} else {
if (offLeftSide(x)) {
// If the parent is a different orientation than the current one, then
// just push it over instead of trying the other side.
if (parentOrientation != orientation) {
x = allowedRect.left;
} else {
final double newX = anchorRect.right + alignmentOffset.dx;
if (!offRightSide(newX)) {
x = newX;
} else {
x = allowedRect.left;
}
}
} else if (offRightSide(x)) {
if (parentOrientation != orientation) {
x = allowedRect.right - childSize.width; // Use childSize
} else {
final double newX = anchorRect.left -
childSize.width -
alignmentOffset.dx; // Use childSize
if (!offLeftSide(newX)) {
x = newX;
} else {
x = allowedRect.right - childSize.width; // Use childSize
}
}
}
}
if (childSize.height >= allowedRect.height) {
// Use childSize
// Too tall to fit, fit as much on as possible.
y = allowedRect.top;
} else {
if (offTop(y)) {
final double newY = anchorRect.bottom;
if (!offBottom(newY)) {
y = newY;
} else {
y = allowedRect.top;
}
} else if (offBottom(y)) {
final double newY = anchorRect.top - childSize.height;
if (!offTop(newY)) {
// Only move the menu up if its parent is horizontal (MenuAnchor/MenuBar).
if (parentOrientation == Axis.horizontal) {
y = newY - alignmentOffset.dy - 8; //for margin
} else {
y = newY;
}
} else {
y = allowedRect.bottom - childSize.height; // Use childSize
}
} else {
y += 8; //for margin if dialog is below button
}
}
return Offset(x, y);
}
@override
bool shouldRelayout(CustomDialogLayout oldDelegate) {
return anchorRect != oldDelegate.anchorRect ||
textDirection != oldDelegate.textDirection ||
alignment != oldDelegate.alignment ||
alignmentOffset != oldDelegate.alignmentOffset ||
menuPadding != oldDelegate.menuPadding ||
orientation != oldDelegate.orientation ||
parentOrientation != oldDelegate.parentOrientation ||
!setEquals(avoidBounds, oldDelegate.avoidBounds);
}
Rect _closestScreen(Iterable<Rect> screens, Offset point) {
Rect closest = screens.first;
for (final Rect screen in screens) {
if ((screen.center - point).distance <
(closest.center - point).distance) {
closest = screen;
}
}
return closest;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment