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