flutter_root_context_menu 0.5.0 copy "flutter_root_context_menu: ^0.5.0" to clipboard
flutter_root_context_menu: ^0.5.0 copied to clipboard

A customizable context menu package for Flutter with animation support, flexible styling, and web-like behavior.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter_root_context_menu/flutter_root_context_menu.dart';

import 'playground_content.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Context Menu Playground',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      navigatorObservers: [ContextMenuRouteObserver()],
      home: const PlaygroundPage(),
    );
  }
}

class PlaygroundPage extends StatefulWidget {
  const PlaygroundPage({super.key});

  @override
  State<PlaygroundPage> createState() => _PlaygroundPageState();
}

class _PlaygroundPageState extends State<PlaygroundPage> {
  // Animation settings
  ContextMenuAnimationBuilder _selectedAnimation = ContextMenuAnimations.popup;
  String _animationName = 'Popup';
  double _animationDuration = 200;

  // Style settings
  Color _backgroundColor = Colors.white;
  Color _hoverColor = const Color(0xFFE0E0E0);
  Color _textColor = Colors.black;
  double _elevation = 8.0;
  double _itemHeight = 40.0;
  double _minWidth = 180.0;
  double _maxWidth = 280.0;

  // Screen padding settings
  double _paddingTop = 0;
  double _paddingBottom = 10;
  double _paddingLeft = 0;
  double _paddingRight = 0;

  // Item styling settings
  double _itemBorderRadius = 0;
  double _itemMarginHorizontal = 0;
  double _itemMarginVertical = 0;

  // Divider and menu styling settings
  double _dividerMarginVertical = 0;
  double _menuPaddingHorizontal = 0;
  double _menuPaddingVertical = 0;

  // BoxShadow settings
  bool _useCustomBoxShadow = false;
  double _shadowBlurRadius = 10.0;
  double _shadowOffsetX = 0.0;
  double _shadowOffsetY = 4.0;
  double _shadowSpreadRadius = 0.0;
  double _shadowOpacity = 0.2;
  Color _shadowColor = Colors.black;

  // Last action
  String _lastAction = 'Right-click in the playground area';

  final Map<String, ContextMenuAnimationBuilder> _animations = {
    'Popup': ContextMenuAnimations.popup,
    'Fade': ContextMenuAnimations.fade,
    'Slide Up': ContextMenuAnimations.slideUp,
    'Slide Down': ContextMenuAnimations.slideDown,
    'Slide Right': ContextMenuAnimations.slideRight,
    'Bounce': ContextMenuAnimations.bounce,
    'Scale': ContextMenuAnimations.scale,
    'None': ContextMenuAnimations.none,
  };

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('Context Menu Playground'),
      ),
      body: LayoutBuilder(
        builder: (context, constraints) {
          final isWide = constraints.maxWidth > 800;

          if (isWide) {
            // Desktop layout: side by side
            return Row(
              children: [
                SizedBox(width: 300, child: _buildControlPanel()),
                const VerticalDivider(width: 1),
                Expanded(child: _buildPlayground()),
              ],
            );
          } else {
            // Mobile layout: stacked
            return Column(
              children: [
                Expanded(flex: 2, child: _buildPlayground()),
                const Divider(height: 1),
                Expanded(flex: 3, child: _buildControlPanel()),
              ],
            );
          }
        },
      ),
    );
  }

  Widget _buildControlPanel() {
    return Container(
      color: Colors.grey.shade50,
      child: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          const Text(
            'Control Panel',
            style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 20),

          // Animation Type
          const Text(
            'Animation Type',
            style: TextStyle(fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          DropdownButton<String>(
            value: _animationName,
            isExpanded: true,
            items: _animations.keys.map((name) {
              return DropdownMenuItem(value: name, child: Text(name));
            }).toList(),
            onChanged: (value) {
              setState(() {
                _animationName = value!;
                _selectedAnimation = _animations[value]!;
              });
            },
          ),
          const SizedBox(height: 20),

          // Animation Duration
          const Text(
            'Animation Duration',
            style: TextStyle(fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          Row(
            children: [
              Expanded(
                child: Slider(
                  value: _animationDuration,
                  min: 0,
                  max: 1000,
                  divisions: 20,
                  label: '${_animationDuration.toInt()}ms',
                  onChanged: (value) {
                    setState(() {
                      _animationDuration = value;
                    });
                  },
                ),
              ),
              Text('${_animationDuration.toInt()}ms'),
            ],
          ),
          const SizedBox(height: 20),

          // Background Color
          const Text(
            'Background Color',
            style: TextStyle(fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          Wrap(
            spacing: 8,
            children: [
              _colorButton(Colors.white, 'White'),
              _colorButton(const Color(0xFF2C2C2C), 'Dark'),
              _colorButton(Colors.blue.shade50, 'Blue'),
              _colorButton(Colors.green.shade50, 'Green'),
              _colorButton(Colors.purple.shade50, 'Purple'),
            ],
          ),
          const SizedBox(height: 20),

          // Text Color
          const Text(
            'Text Color',
            style: TextStyle(fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          Wrap(
            spacing: 8,
            children: [
              _colorButton(Colors.black, 'Black', isTextColor: true),
              _colorButton(Colors.white, 'White', isTextColor: true),
              _colorButton(Colors.blue.shade900, 'Blue', isTextColor: true),
              _colorButton(Colors.green.shade900, 'Green', isTextColor: true),
              _colorButton(Colors.red, 'Red', isTextColor: true),
            ],
          ),
          const SizedBox(height: 20),

          // Elevation
          const Text(
            'Elevation',
            style: TextStyle(fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          Row(
            children: [
              Expanded(
                child: Slider(
                  value: _elevation,
                  min: 0,
                  max: 24,
                  divisions: 12,
                  label: _elevation.toInt().toString(),
                  onChanged: (value) {
                    setState(() {
                      _elevation = value;
                    });
                  },
                ),
              ),
              Text(_elevation.toInt().toString()),
            ],
          ),
          const SizedBox(height: 20),

          // Item Height
          const Text(
            'Item Height',
            style: TextStyle(fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          Row(
            children: [
              Expanded(
                child: Slider(
                  value: _itemHeight,
                  min: 30,
                  max: 60,
                  divisions: 6,
                  label: _itemHeight.toInt().toString(),
                  onChanged: (value) {
                    setState(() {
                      _itemHeight = value;
                    });
                  },
                ),
              ),
              Text(_itemHeight.toInt().toString()),
            ],
          ),
          const SizedBox(height: 20),

          // Min Width
          const Text(
            'Min Width',
            style: TextStyle(fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          Row(
            children: [
              Expanded(
                child: Slider(
                  value: _minWidth,
                  min: 100,
                  max: 400,
                  divisions: 30,
                  label: _minWidth.toInt().toString(),
                  onChanged: (value) {
                    setState(() {
                      _minWidth = value;
                      if (_minWidth > _maxWidth) {
                        _maxWidth = _minWidth;
                      }
                    });
                  },
                ),
              ),
              Text(_minWidth.toInt().toString()),
            ],
          ),
          const SizedBox(height: 20),

          // Max Width
          const Text(
            'Max Width',
            style: TextStyle(fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          Row(
            children: [
              Expanded(
                child: Slider(
                  value: _maxWidth,
                  min: 100,
                  max: 400,
                  divisions: 30,
                  label: _maxWidth.toInt().toString(),
                  onChanged: (value) {
                    setState(() {
                      _maxWidth = value;
                      if (_maxWidth < _minWidth) {
                        _minWidth = _maxWidth;
                      }
                    });
                  },
                ),
              ),
              Text(_maxWidth.toInt().toString()),
            ],
          ),
          const SizedBox(height: 20),

          // Screen Padding Section
          const Text(
            'Screen Padding',
            style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
          ),
          const SizedBox(height: 12),

          // Padding Top
          const Text('Top', style: TextStyle(fontWeight: FontWeight.w500)),
          const SizedBox(height: 4),
          Row(
            children: [
              Expanded(
                child: Slider(
                  value: _paddingTop,
                  min: 0,
                  max: 100,
                  divisions: 20,
                  label: _paddingTop.toInt().toString(),
                  onChanged: (value) {
                    setState(() {
                      _paddingTop = value;
                    });
                  },
                ),
              ),
              SizedBox(width: 40, child: Text('${_paddingTop.toInt()}px')),
            ],
          ),
          const SizedBox(height: 12),

          // Padding Bottom
          const Text('Bottom', style: TextStyle(fontWeight: FontWeight.w500)),
          const SizedBox(height: 4),
          Row(
            children: [
              Expanded(
                child: Slider(
                  value: _paddingBottom,
                  min: 0,
                  max: 100,
                  divisions: 20,
                  label: _paddingBottom.toInt().toString(),
                  onChanged: (value) {
                    setState(() {
                      _paddingBottom = value;
                    });
                  },
                ),
              ),
              SizedBox(width: 40, child: Text('${_paddingBottom.toInt()}px')),
            ],
          ),
          const SizedBox(height: 12),

          // Padding Left
          const Text('Left', style: TextStyle(fontWeight: FontWeight.w500)),
          const SizedBox(height: 4),
          Row(
            children: [
              Expanded(
                child: Slider(
                  value: _paddingLeft,
                  min: 0,
                  max: 100,
                  divisions: 20,
                  label: _paddingLeft.toInt().toString(),
                  onChanged: (value) {
                    setState(() {
                      _paddingLeft = value;
                    });
                  },
                ),
              ),
              SizedBox(width: 40, child: Text('${_paddingLeft.toInt()}px')),
            ],
          ),
          const SizedBox(height: 12),

          // Padding Right
          const Text('Right', style: TextStyle(fontWeight: FontWeight.w500)),
          const SizedBox(height: 4),
          Row(
            children: [
              Expanded(
                child: Slider(
                  value: _paddingRight,
                  min: 0,
                  max: 100,
                  divisions: 20,
                  label: _paddingRight.toInt().toString(),
                  onChanged: (value) {
                    setState(() {
                      _paddingRight = value;
                    });
                  },
                ),
              ),
              SizedBox(width: 40, child: Text('${_paddingRight.toInt()}px')),
            ],
          ),
          const SizedBox(height: 20),

          // Item Styling Section
          const Text(
            'Item Styling',
            style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
          ),
          const SizedBox(height: 12),

          // Item Border Radius
          const Text(
            'Border Radius',
            style: TextStyle(fontWeight: FontWeight.w500),
          ),
          const SizedBox(height: 4),
          Row(
            children: [
              Expanded(
                child: Slider(
                  value: _itemBorderRadius,
                  min: 0,
                  max: 20,
                  divisions: 20,
                  label: _itemBorderRadius.toInt().toString(),
                  onChanged: (value) {
                    setState(() {
                      _itemBorderRadius = value;
                    });
                  },
                ),
              ),
              SizedBox(
                width: 40,
                child: Text('${_itemBorderRadius.toInt()}px'),
              ),
            ],
          ),
          const SizedBox(height: 12),

          // Item Margin Horizontal
          const Text(
            'Margin Horizontal',
            style: TextStyle(fontWeight: FontWeight.w500),
          ),
          const SizedBox(height: 4),
          Row(
            children: [
              Expanded(
                child: Slider(
                  value: _itemMarginHorizontal,
                  min: 0,
                  max: 20,
                  divisions: 20,
                  label: _itemMarginHorizontal.toInt().toString(),
                  onChanged: (value) {
                    setState(() {
                      _itemMarginHorizontal = value;
                    });
                  },
                ),
              ),
              SizedBox(
                width: 40,
                child: Text('${_itemMarginHorizontal.toInt()}px'),
              ),
            ],
          ),
          const SizedBox(height: 12),

          // Item Margin Vertical
          const Text(
            'Margin Vertical',
            style: TextStyle(fontWeight: FontWeight.w500),
          ),
          const SizedBox(height: 4),
          Row(
            children: [
              Expanded(
                child: Slider(
                  value: _itemMarginVertical,
                  min: 0,
                  max: 10,
                  divisions: 20,
                  label: _itemMarginVertical.toInt().toString(),
                  onChanged: (value) {
                    setState(() {
                      _itemMarginVertical = value;
                    });
                  },
                ),
              ),
              SizedBox(
                width: 40,
                child: Text('${_itemMarginVertical.toInt()}px'),
              ),
            ],
          ),
          const SizedBox(height: 20),

          // Divider & Menu Section
          const Text(
            'Divider & Menu',
            style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
          ),
          const SizedBox(height: 12),

          // Divider Margin Vertical
          const Text(
            'Divider Margin Vertical',
            style: TextStyle(fontWeight: FontWeight.w500),
          ),
          const SizedBox(height: 4),
          Row(
            children: [
              Expanded(
                child: Slider(
                  value: _dividerMarginVertical,
                  min: 0,
                  max: 20,
                  divisions: 20,
                  label: _dividerMarginVertical.toInt().toString(),
                  onChanged: (value) {
                    setState(() {
                      _dividerMarginVertical = value;
                    });
                  },
                ),
              ),
              SizedBox(
                width: 40,
                child: Text('${_dividerMarginVertical.toInt()}px'),
              ),
            ],
          ),
          const SizedBox(height: 12),

          // Menu Padding Horizontal
          const Text(
            'Menu Padding Horizontal',
            style: TextStyle(fontWeight: FontWeight.w500),
          ),
          const SizedBox(height: 4),
          Row(
            children: [
              Expanded(
                child: Slider(
                  value: _menuPaddingHorizontal,
                  min: 0,
                  max: 20,
                  divisions: 20,
                  label: _menuPaddingHorizontal.toInt().toString(),
                  onChanged: (value) {
                    setState(() {
                      _menuPaddingHorizontal = value;
                    });
                  },
                ),
              ),
              SizedBox(
                width: 40,
                child: Text('${_menuPaddingHorizontal.toInt()}px'),
              ),
            ],
          ),
          const SizedBox(height: 12),

          // Menu Padding Vertical
          const Text(
            'Menu Padding Vertical',
            style: TextStyle(fontWeight: FontWeight.w500),
          ),
          const SizedBox(height: 4),
          Row(
            children: [
              Expanded(
                child: Slider(
                  value: _menuPaddingVertical,
                  min: 0,
                  max: 20,
                  divisions: 20,
                  label: _menuPaddingVertical.toInt().toString(),
                  onChanged: (value) {
                    setState(() {
                      _menuPaddingVertical = value;
                    });
                  },
                ),
              ),
              SizedBox(
                width: 40,
                child: Text('${_menuPaddingVertical.toInt()}px'),
              ),
            ],
          ),
          const SizedBox(height: 20),

          // BoxShadow Section
          const Text(
            'Box Shadow',
            style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
          ),
          const SizedBox(height: 12),

          // Use Custom BoxShadow Toggle
          Row(
            children: [
              const Text(
                'Use Custom Shadow',
                style: TextStyle(fontWeight: FontWeight.w500),
              ),
              const Spacer(),
              Switch(
                value: _useCustomBoxShadow,
                onChanged: (value) {
                  setState(() {
                    _useCustomBoxShadow = value;
                  });
                },
              ),
            ],
          ),
          const SizedBox(height: 12),

          if (_useCustomBoxShadow) ...[
            // Blur Radius
            const Text(
              'Blur Radius',
              style: TextStyle(fontWeight: FontWeight.w500),
            ),
            const SizedBox(height: 4),
            Row(
              children: [
                Expanded(
                  child: Slider(
                    value: _shadowBlurRadius,
                    min: 0,
                    max: 30,
                    divisions: 30,
                    label: _shadowBlurRadius.toInt().toString(),
                    onChanged: (value) {
                      setState(() {
                        _shadowBlurRadius = value;
                      });
                    },
                  ),
                ),
                SizedBox(
                  width: 40,
                  child: Text('${_shadowBlurRadius.toInt()}px'),
                ),
              ],
            ),
            const SizedBox(height: 12),

            // Spread Radius
            const Text(
              'Spread Radius',
              style: TextStyle(fontWeight: FontWeight.w500),
            ),
            const SizedBox(height: 4),
            Row(
              children: [
                Expanded(
                  child: Slider(
                    value: _shadowSpreadRadius,
                    min: -10,
                    max: 10,
                    divisions: 20,
                    label: _shadowSpreadRadius.toInt().toString(),
                    onChanged: (value) {
                      setState(() {
                        _shadowSpreadRadius = value;
                      });
                    },
                  ),
                ),
                SizedBox(
                  width: 40,
                  child: Text('${_shadowSpreadRadius.toInt()}px'),
                ),
              ],
            ),
            const SizedBox(height: 12),

            // Offset X
            const Text(
              'Offset X',
              style: TextStyle(fontWeight: FontWeight.w500),
            ),
            const SizedBox(height: 4),
            Row(
              children: [
                Expanded(
                  child: Slider(
                    value: _shadowOffsetX,
                    min: -20,
                    max: 20,
                    divisions: 40,
                    label: _shadowOffsetX.toInt().toString(),
                    onChanged: (value) {
                      setState(() {
                        _shadowOffsetX = value;
                      });
                    },
                  ),
                ),
                SizedBox(width: 40, child: Text('${_shadowOffsetX.toInt()}px')),
              ],
            ),
            const SizedBox(height: 12),

            // Offset Y
            const Text(
              'Offset Y',
              style: TextStyle(fontWeight: FontWeight.w500),
            ),
            const SizedBox(height: 4),
            Row(
              children: [
                Expanded(
                  child: Slider(
                    value: _shadowOffsetY,
                    min: -20,
                    max: 20,
                    divisions: 40,
                    label: _shadowOffsetY.toInt().toString(),
                    onChanged: (value) {
                      setState(() {
                        _shadowOffsetY = value;
                      });
                    },
                  ),
                ),
                SizedBox(width: 40, child: Text('${_shadowOffsetY.toInt()}px')),
              ],
            ),
            const SizedBox(height: 12),

            // Opacity
            const Text(
              'Shadow Opacity',
              style: TextStyle(fontWeight: FontWeight.w500),
            ),
            const SizedBox(height: 4),
            Row(
              children: [
                Expanded(
                  child: Slider(
                    value: _shadowOpacity,
                    min: 0,
                    max: 1,
                    divisions: 20,
                    label: _shadowOpacity.toStringAsFixed(2),
                    onChanged: (value) {
                      setState(() {
                        _shadowOpacity = value;
                      });
                    },
                  ),
                ),
                SizedBox(
                  width: 40,
                  child: Text(_shadowOpacity.toStringAsFixed(2)),
                ),
              ],
            ),
            const SizedBox(height: 12),

            // Shadow Color
            const Text(
              'Shadow Color',
              style: TextStyle(fontWeight: FontWeight.w500),
            ),
            const SizedBox(height: 8),
            Wrap(
              spacing: 8,
              children: [
                _shadowColorButton(Colors.black, 'Black'),
                _shadowColorButton(Colors.blue.shade900, 'Blue'),
                _shadowColorButton(Colors.red.shade900, 'Red'),
                _shadowColorButton(Colors.purple.shade900, 'Purple'),
              ],
            ),
          ],
        ],
      ),
    );
  }

  Widget _shadowColorButton(Color color, String label) {
    final isSelected = _shadowColor == color;

    return GestureDetector(
      onTap: () {
        setState(() {
          _shadowColor = color;
        });
      },
      child: Container(
        padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
        decoration: BoxDecoration(
          color: color,
          borderRadius: BorderRadius.circular(4),
          border: Border.all(
            color: isSelected ? Colors.orange : Colors.grey.shade300,
            width: isSelected ? 2 : 1,
          ),
        ),
        child: Text(
          label,
          style: const TextStyle(color: Colors.white, fontSize: 12),
        ),
      ),
    );
  }

  Widget _colorButton(Color color, String label, {bool isTextColor = false}) {
    final isSelected = isTextColor
        ? _textColor == color
        : _backgroundColor == color;

    return GestureDetector(
      onTap: () {
        setState(() {
          if (isTextColor) {
            _textColor = color;
            // Auto-adjust hover color
            if (color == Colors.white) {
              _hoverColor = Colors.grey.shade700;
            } else {
              _hoverColor = _backgroundColor == Colors.white
                  ? const Color(0xFFE0E0E0)
                  : Colors.white.withValues(alpha: 0.1);
            }
          } else {
            _backgroundColor = color;
            // Auto-adjust hover color
            if (color == Colors.white) {
              _hoverColor = const Color(0xFFE0E0E0);
            } else {
              _hoverColor = Colors.white.withValues(alpha: 0.1);
            }
          }
        });
      },
      child: Container(
        padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
        decoration: BoxDecoration(
          color: color,
          borderRadius: BorderRadius.circular(4),
          border: Border.all(
            color: isSelected ? Colors.blue : Colors.grey.shade300,
            width: isSelected ? 2 : 1,
          ),
        ),
        child: Text(
          label,
          style: TextStyle(
            color: color.computeLuminance() > 0.5 ? Colors.black : Colors.white,
            fontSize: 12,
            fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
          ),
        ),
      ),
    );
  }

  Widget _buildPlayground() {
    return Container(
      color: Colors.grey.shade100,
      padding: const EdgeInsets.all(40),
      child: Container(
        decoration: BoxDecoration(
          border: Border.all(color: Colors.red, width: 2),
        ),
        // ContextMenuArea wraps the separated widget
        // PlaygroundContent can use its own context since it's inside ContextMenuArea
        child: ContextMenuArea(
          child: PlaygroundContent(
            lastAction: _lastAction,
            onActionChanged: (action) => setState(() => _lastAction = action),
            config: ContextMenuConfig(
              backgroundColor: _backgroundColor,
              hoverColor: _hoverColor,
              textStyle: TextStyle(color: _textColor, fontSize: 14),
              elevation: _elevation,
              itemHeight: _itemHeight,
              minWidth: _minWidth,
              maxWidth: _maxWidth,
              animationBuilder: _selectedAnimation,
              animationDuration: Duration(
                milliseconds: _animationDuration.toInt(),
              ),
              screenPadding: EdgeInsets.only(
                top: _paddingTop,
                bottom: _paddingBottom,
                left: _paddingLeft,
                right: _paddingRight,
              ),
              itemBorderRadius: BorderRadius.circular(_itemBorderRadius),
              itemMargin: EdgeInsets.symmetric(
                horizontal: _itemMarginHorizontal,
                vertical: _itemMarginVertical,
              ),
              dividerMargin: EdgeInsets.symmetric(
                vertical: _dividerMarginVertical,
              ),
              menuPadding: EdgeInsets.symmetric(
                horizontal: _menuPaddingHorizontal,
                vertical: _menuPaddingVertical,
              ),
              boxShadow: _useCustomBoxShadow
                  ? [
                      BoxShadow(
                        color: _shadowColor.withValues(alpha: _shadowOpacity),
                        blurRadius: _shadowBlurRadius,
                        spreadRadius: _shadowSpreadRadius,
                        offset: Offset(_shadowOffsetX, _shadowOffsetY),
                      ),
                    ]
                  : null,
            ),
          ),
        ),
      ),
    );
  }
}

/// Second page to demonstrate ContextMenuRouteObserver in action
class SecondPage extends StatelessWidget {
  const SecondPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('Second Screen'),
      ),
      body: ContextMenuArea(
        child: GestureDetector(
          onSecondaryTapDown: (details) {
            showRootContextMenu(
              context: context,
              position: details.globalPosition,
              items: [
                ContextMenuItem(
                  label: 'Go Back',
                  icon: const Icon(Icons.arrow_back, size: 18),
                  onTap: () {
                    Navigator.pop(context);
                  },
                ),
                ContextMenuItem.divider(),
                ContextMenuItem(
                  label: 'Action 1',
                  icon: const Icon(Icons.star, size: 18),
                  onTap: () {},
                ),
                ContextMenuItem(
                  label: 'Action 2',
                  icon: const Icon(Icons.favorite, size: 18),
                  onTap: () {},
                ),
              ],
            );
          },
          child: Container(
            color: Colors.blue.shade50,
            child: Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Icon(
                    Icons.check_circle,
                    size: 80,
                    color: Colors.green.shade400,
                  ),
                  const SizedBox(height: 24),
                  const Text(
                    'Route Observer Test',
                    style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
                  ),
                  const SizedBox(height: 16),
                  Container(
                    constraints: const BoxConstraints(maxWidth: 400),
                    padding: const EdgeInsets.all(20),
                    margin: const EdgeInsets.symmetric(horizontal: 20),
                    decoration: BoxDecoration(
                      color: Colors.white,
                      borderRadius: BorderRadius.circular(12),
                      boxShadow: [
                        BoxShadow(
                          color: Colors.black.withValues(alpha: 0.1),
                          blurRadius: 10,
                          offset: const Offset(0, 4),
                        ),
                      ],
                    ),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Row(
                          children: [
                            Icon(
                              Icons.info_outline,
                              color: Colors.blue.shade700,
                            ),
                            const SizedBox(width: 8),
                            const Text(
                              'How to Test:',
                              style: TextStyle(
                                fontSize: 18,
                                fontWeight: FontWeight.bold,
                              ),
                            ),
                          ],
                        ),
                        const SizedBox(height: 16),
                        _buildStep('1', 'Go back to the previous screen'),
                        const SizedBox(height: 8),
                        _buildStep('2', 'Right-click to open the context menu'),
                        const SizedBox(height: 8),
                        _buildStep('3', 'Navigate here again (menu or button)'),
                        const SizedBox(height: 8),
                        _buildStep('4', 'Notice: Menu closes automatically!'),
                        const SizedBox(height: 16),
                        Container(
                          padding: const EdgeInsets.all(12),
                          decoration: BoxDecoration(
                            color: Colors.green.shade50,
                            borderRadius: BorderRadius.circular(8),
                            border: Border.all(color: Colors.green.shade200),
                          ),
                          child: Row(
                            children: [
                              Icon(
                                Icons.check,
                                color: Colors.green.shade700,
                                size: 20,
                              ),
                              const SizedBox(width: 8),
                              Expanded(
                                child: Text(
                                  'ContextMenuRouteObserver is working!',
                                  style: TextStyle(
                                    color: Colors.green.shade700,
                                    fontWeight: FontWeight.w500,
                                  ),
                                ),
                              ),
                            ],
                          ),
                        ),
                      ],
                    ),
                  ),
                  const SizedBox(height: 30),
                  ElevatedButton.icon(
                    onPressed: () {
                      Navigator.pop(context);
                    },
                    icon: const Icon(Icons.arrow_back),
                    label: const Text('Go Back'),
                    style: ElevatedButton.styleFrom(
                      padding: const EdgeInsets.symmetric(
                        horizontal: 32,
                        vertical: 16,
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }

  Widget _buildStep(String number, String text) {
    return Row(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Container(
          width: 24,
          height: 24,
          decoration: BoxDecoration(
            color: Colors.blue.shade700,
            shape: BoxShape.circle,
          ),
          child: Center(
            child: Text(
              number,
              style: const TextStyle(
                color: Colors.white,
                fontSize: 12,
                fontWeight: FontWeight.bold,
              ),
            ),
          ),
        ),
        const SizedBox(width: 12),
        Expanded(
          child: Padding(
            padding: const EdgeInsets.only(top: 2),
            child: Text(text, style: const TextStyle(fontSize: 14)),
          ),
        ),
      ],
    );
  }
}
0
likes
160
points
224
downloads

Publisher

unverified uploader

Weekly Downloads

A customizable context menu package for Flutter with animation support, flexible styling, and web-like behavior.

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

flutter

More

Packages that depend on flutter_root_context_menu