Last active
June 4, 2025 03:58
-
-
Save rubywai/44d0fc3a42a84a4c16f7a09cc6501eab to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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