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
117 lines
3.8 KiB
Dart
117 lines
3.8 KiB
Dart
import "package:flutter/widgets.dart";
|
|
|
|
import "menu.dart";
|
|
|
|
const double _kContextMenuGap = 8.0;
|
|
const double _kContextMenuScreenPadding = 8.0;
|
|
const double _kContextMenuMinWidth = 192.0;
|
|
|
|
// shows a right-click style context menu at [globalPosition]. used to lean on
|
|
// shadcn's OverlayManager.showMenu; now its a plain flutter Overlay entry with
|
|
// a transparent tap-catcher to dismiss.
|
|
void showContextMenu({
|
|
required BuildContext context,
|
|
required Offset globalPosition,
|
|
required List<MenuItem> items,
|
|
required VoidCallback onDismissed,
|
|
}) {
|
|
final overlay = Overlay.of(context, rootOverlay: true);
|
|
final overlayBox = overlay.context.findRenderObject() as RenderBox?;
|
|
final overlayPosition =
|
|
overlayBox?.globalToLocal(globalPosition) ?? globalPosition;
|
|
|
|
late OverlayEntry entry;
|
|
var closed = false;
|
|
void close() {
|
|
if (closed) return;
|
|
closed = true;
|
|
entry.remove();
|
|
onDismissed();
|
|
}
|
|
|
|
entry = OverlayEntry(
|
|
builder: (ctx) {
|
|
return Stack(
|
|
children: [
|
|
// tap anywhere outside to dismiss.
|
|
Positioned.fill(
|
|
child: GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
excludeFromSemantics: true,
|
|
onTap: close,
|
|
child: const SizedBox.expand(),
|
|
),
|
|
),
|
|
CustomSingleChildLayout(
|
|
delegate: _ContextMenuLayoutDelegate(position: overlayPosition),
|
|
child: MenuGroup(
|
|
direction: Axis.vertical,
|
|
subMenuOffset: const Offset(8, -4),
|
|
onDismissed: close,
|
|
builder: (ctx, children) => MenuPopup(children: children),
|
|
children: items,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
|
|
overlay.insert(entry);
|
|
}
|
|
|
|
class _ContextMenuLayoutDelegate extends SingleChildLayoutDelegate {
|
|
const _ContextMenuLayoutDelegate({required this.position});
|
|
|
|
final Offset position;
|
|
|
|
@override
|
|
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
|
|
final maxWidth = (constraints.maxWidth - _kContextMenuScreenPadding * 2)
|
|
.clamp(0.0, double.infinity);
|
|
final maxHeight = (constraints.maxHeight - _kContextMenuScreenPadding * 2)
|
|
.clamp(0.0, double.infinity);
|
|
return BoxConstraints(
|
|
minWidth: _kContextMenuMinWidth.clamp(0.0, maxWidth),
|
|
maxWidth: maxWidth,
|
|
maxHeight: maxHeight,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Offset getPositionForChild(Size size, Size childSize) {
|
|
final minLeft = _kContextMenuScreenPadding;
|
|
final minTop = _kContextMenuScreenPadding;
|
|
final maxLeft = size.width - _kContextMenuScreenPadding - childSize.width;
|
|
final maxTop = size.height - _kContextMenuScreenPadding - childSize.height;
|
|
|
|
final preferredRight = position.dx + _kContextMenuGap;
|
|
final preferredLeft = position.dx - _kContextMenuGap - childSize.width;
|
|
final rightFits =
|
|
preferredRight + childSize.width <=
|
|
size.width - _kContextMenuScreenPadding;
|
|
final leftFits = preferredLeft >= _kContextMenuScreenPadding;
|
|
final opensRight = rightFits || !leftFits;
|
|
final rawLeft = opensRight ? preferredRight : preferredLeft;
|
|
|
|
final preferredBelow = position.dy;
|
|
final preferredAbove = position.dy - childSize.height;
|
|
final belowFits =
|
|
preferredBelow + childSize.height <=
|
|
size.height - _kContextMenuScreenPadding;
|
|
final aboveFits = preferredAbove >= _kContextMenuScreenPadding;
|
|
final opensBelow = belowFits || !aboveFits;
|
|
final rawTop = opensBelow ? preferredBelow : preferredAbove;
|
|
|
|
return Offset(
|
|
rawLeft.clamp(minLeft, maxLeft.clamp(minLeft, double.infinity)),
|
|
rawTop.clamp(minTop, maxTop.clamp(minTop, double.infinity)),
|
|
);
|
|
}
|
|
|
|
@override
|
|
bool shouldRelayout(_ContextMenuLayoutDelegate oldDelegate) {
|
|
return position != oldDelegate.position;
|
|
}
|
|
}
|