showAsrPopupMenu function

Future<void> showAsrPopupMenu({
  1. required BuildContext context,
  2. required GlobalKey<State<StatefulWidget>> targetKey,
  3. required List<AsrPopupMenuAction> actions,
  4. bool isSelf = false,
})

Show ASR popup menu above the target widget Returns when the menu is dismissed

Implementation

Future<void> showAsrPopupMenu({
  required BuildContext context,
  required GlobalKey targetKey,
  required List<AsrPopupMenuAction> actions,
  bool isSelf = false,
}) async {
  final RenderBox? renderBox = targetKey.currentContext?.findRenderObject() as RenderBox?;
  if (renderBox == null) return;

  final Offset targetPosition = renderBox.localToGlobal(Offset.zero);
  final Size targetSize = renderBox.size;
  final Size screenSize = MediaQuery.of(context).size;

  // Calculate menu position (above the target)
  // Menu width estimation: ~3 items * 60px each = ~180px
  // Menu height: icon(20) + spacing(4) + text(~12) + padding(8*2) = ~52px
  const double menuWidth = 180;
  const double menuHeight = 52;
  const double verticalOffset = 4;

  double left;
  if (isSelf) {
    // For self messages, align to the right
    left = targetPosition.dx + targetSize.width - menuWidth;
  } else {
    // For other messages, align to the left
    left = targetPosition.dx;
  }

  // Ensure menu stays within screen bounds
  if (left < 8) left = 8;
  if (left + menuWidth > screenSize.width - 8) {
    left = screenSize.width - menuWidth - 8;
  }

  // Position above the target
  double top = targetPosition.dy - menuHeight - verticalOffset;
  // If not enough space above, show below
  if (top < MediaQuery.of(context).padding.top + 8) {
    top = targetPosition.dy + targetSize.height + verticalOffset;
  }

  final OverlayState overlayState = Overlay.of(context);
  late OverlayEntry overlayEntry;

  overlayEntry = OverlayEntry(
    builder: (context) {
      return Stack(
        children: [
          // Transparent barrier to dismiss menu
          Positioned.fill(
            child: GestureDetector(
              onTap: () => overlayEntry.remove(),
              behavior: HitTestBehavior.opaque,
              child: Container(color: Colors.transparent),
            ),
          ),
          // Menu
          Positioned(
            left: left,
            top: top,
            child: AsrPopupMenu(
              actions: actions.map((action) {
                return AsrPopupMenuAction(
                  label: action.label,
                  iconAsset: action.iconAsset,
                  onTap: () {
                    overlayEntry.remove();
                    action.onTap();
                  },
                );
              }).toList(),
            ),
          ),
        ],
      );
    },
  );

  overlayState.insert(overlayEntry);
}