The Garage SDKs, in the open
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
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user