import "dart:math" as math; import "package:flutter/widgets.dart"; /// Where a popover should actually be placed, once the space around its /// trigger has been measured. /// /// Feed [targetAnchor]/[followerAnchor]/[offset] straight into a /// [CompositedTransformFollower], and clamp the popup's own box with /// [maxWidth]/[maxHeight]. class PopoverPlacement { const PopoverPlacement({ required this.targetAnchor, required this.followerAnchor, required this.offset, required this.maxWidth, required this.maxHeight, required this.openBelow, required this.openToRight, }); final Alignment targetAnchor; final Alignment followerAnchor; final Offset offset; final double maxWidth; final double maxHeight; /// Which way the flip actually resolved. Callers that draw something /// direction-dependent (a tail, a chevron) need to know. final bool openBelow; final bool openToRight; } /// Which axis a popover moves along relative to its trigger. enum PopoverAxis { /// Drops below the trigger (or flips above). Selects, dropdowns, menubar /// menus - the popup is stacked under the thing that opened it. vertical, /// Flies out to the side of the trigger (or flips to the other side). /// Submenus - the popup sits beside the thing that opened it. horizontal, } /// Works out where a popover fits. /// /// The rule is "keep the preferred side unless it doesn't fit and the other /// side is genuinely better" - NOT "flip whenever the preferred side is /// tight". A popover that flips the moment it's a pixel short would jitter /// between sides as the window resizes, and would flip even when the opposite /// side has less room than the side it left. /// /// [preferredSize] is what the popup would like; when neither side can give it /// that, the roomier side wins and the returned max dimensions clamp the popup /// to what's actually there. Pass [Size.zero] if the size isn't known up front /// (a shrink-wrapping menu) - the flip then falls back to a pure /// which-side-has-more-room comparison. PopoverPlacement resolvePopoverPlacement({ required RenderBox target, required RenderBox overlay, required Size preferredSize, PopoverAxis axis = PopoverAxis.vertical, bool preferBelow = true, bool preferRight = true, double gap = 0.0, double margin = 8.0, }) { final targetOffset = target.localToGlobal(Offset.zero, ancestor: overlay); final overlaySize = overlay.size; final targetSize = target.size; final roomBelow = overlaySize.height - targetOffset.dy - targetSize.height; final roomAbove = targetOffset.dy; final roomRight = overlaySize.width - targetOffset.dx; final roomLeft = targetOffset.dx + targetSize.width; // for a horizontal popover the sideways room is measured from the trigger's // EDGES (it sits beside the trigger), not from its near edge the way a // vertically-stacked popover measures its own left/right alignment room. final roomRightOfTarget = overlaySize.width - targetOffset.dx - targetSize.width; final roomLeftOfTarget = targetOffset.dx; bool fits(double room, double needed) => room >= needed + margin; late final bool openBelow; late final bool openToRight; switch (axis) { case PopoverAxis.vertical: final needed = preferredSize.height + gap; openBelow = preferBelow ? (fits(roomBelow, needed) || roomBelow >= roomAbove) : !(fits(roomAbove, needed) || roomAbove >= roomBelow); // horizontal here is just which way the popup extends from its anchor // corner; it never sits beside the trigger, so it measures from the // near edge. final neededW = preferredSize.width; openToRight = preferRight ? (fits(roomRight, neededW) || roomRight >= roomLeft) : !(fits(roomLeft, neededW) || roomLeft >= roomRight); case PopoverAxis.horizontal: final needed = preferredSize.width + gap; openToRight = preferRight ? (fits(roomRightOfTarget, needed) || roomRightOfTarget >= roomLeftOfTarget) : !(fits(roomLeftOfTarget, needed) || roomLeftOfTarget >= roomRightOfTarget); final neededH = preferredSize.height; openBelow = preferBelow ? (fits(roomBelow + targetSize.height, neededH) || roomBelow >= roomAbove) : !(fits(roomAbove + targetSize.height, neededH) || roomAbove >= roomBelow); } final double availableHeight; final double availableWidth; switch (axis) { case PopoverAxis.vertical: availableHeight = (openBelow ? roomBelow : roomAbove) - gap - margin; availableWidth = (openToRight ? roomRight : roomLeft) - margin; case PopoverAxis.horizontal: // a side-flying popup is free to run the full height of the overlay // from wherever it starts, so its height budget is measured from the // trigger's own top/bottom edge rather than past it availableHeight = (openBelow ? roomBelow + targetSize.height : roomAbove + targetSize.height) - margin; availableWidth = (openToRight ? roomRightOfTarget : roomLeftOfTarget) - gap - margin; } final Alignment targetAnchor; final Alignment followerAnchor; final Offset resolvedOffset; switch (axis) { case PopoverAxis.vertical: targetAnchor = Alignment(openToRight ? -1 : 1, openBelow ? 1 : -1); followerAnchor = Alignment(openToRight ? -1 : 1, openBelow ? -1 : 1); resolvedOffset = Offset(0, openBelow ? gap : -gap); case PopoverAxis.horizontal: targetAnchor = Alignment(openToRight ? 1 : -1, openBelow ? -1 : 1); followerAnchor = Alignment(openToRight ? -1 : 1, openBelow ? -1 : 1); resolvedOffset = Offset(openToRight ? gap : -gap, 0); } return PopoverPlacement( targetAnchor: targetAnchor, followerAnchor: followerAnchor, offset: resolvedOffset, maxWidth: math.max(0.0, availableWidth), maxHeight: math.max(0.0, availableHeight), openBelow: openBelow, openToRight: openToRight, ); }