Files
Garage-SDKs/garage_ui/lib/context_menu.dart
T
ImBenjiandClaude Opus 5.5 b269201919 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
2026-09-23 18:49:21 +01:00

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;
}
}