garage_auth, garage_entitlements, garage_iap and garage_ui, moved out of Garage-Services and Metro-Map-Maker into one public repo. MIT, one readme, docs under docs/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013F4NWNvYcdeSgqbWMT1VQ7
161 lines
6.0 KiB
Dart
161 lines
6.0 KiB
Dart
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,
|
|
);
|
|
}
|