Skip to main content

m3e_menu 0.0.1 copy "m3e_menu: ^0.0.1" to clipboard
m3e_menu: ^0.0.1 copied to clipboard

Material 3 Expressive menus, cascading submenus, segmented groups, and popup menus with spring physics.

example/lib/main.dart

// Copyright (c) 2026 Mudit Purohit
//
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.

import 'package:flutter/material.dart';
import 'package:m3e_buttons/m3e_buttons.dart' hide M3EMotion;
import 'package:m3e_menu/m3e_menu.dart';

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

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

  @override
  State<M3EMenuExampleApp> createState() => _M3EMenuExampleAppState();
}

class _M3EMenuExampleAppState extends State<M3EMenuExampleApp> {
  ThemeMode _themeMode = ThemeMode.light;

  void _toggleTheme() {
    setState(() {
      _themeMode = _themeMode == ThemeMode.light
          ? ThemeMode.dark
          : ThemeMode.light;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'M3E Menu Showcase',
      themeMode: _themeMode,
      theme: ThemeData(
        colorSchemeSeed: Colors.indigo,
        useMaterial3: true,
        brightness: Brightness.light,
      ),
      darkTheme: ThemeData(
        colorSchemeSeed: Colors.indigo,
        useMaterial3: true,
        brightness: Brightness.dark,
      ),
      home: M3EMenuShowcaseScreen(
        isDarkMode: _themeMode == ThemeMode.dark,
        onToggleTheme: _toggleTheme,
      ),
    );
  }
}

class M3EMenuShowcaseScreen extends StatefulWidget {
  final bool isDarkMode;
  final VoidCallback onToggleTheme;

  const M3EMenuShowcaseScreen({
    super.key,
    required this.isDarkMode,
    required this.onToggleTheme,
  });

  @override
  State<M3EMenuShowcaseScreen> createState() => _M3EMenuShowcaseScreenState();
}

class _M3EMenuShowcaseScreenState extends State<M3EMenuShowcaseScreen> {
  final M3EMenuController _fileMenuController = M3EMenuController();
  final M3EMenuController _optionsMenuController = M3EMenuController();
  final M3EMenuController _styledMenuController = M3EMenuController();

  // Settings & state
  bool _showLineNumbers = true;
  bool _autoSave = true;
  bool _wordWrap = false;
  String _layoutDensity = 'Comfortable';
  String _lastEvent = 'No actions performed yet';

  // Configurable Playground State
  M3EMenuPosition _selectedPosition = M3EMenuPosition.under;
  M3EMenuAnimationStyle _selectedAnimStyle = M3EMenuAnimationStyle.popup;
  double _elevation = 3.0;
  String _motionPreset = 'Expressive Fast';

  M3EMotion get _currentMotion {
    switch (_motionPreset) {
      case 'Expressive Slow':
        return M3EMotion.expressiveSpatialSlow;
      case 'Standard Default':
        return M3EMotion.standardSpatialDefault;
      case 'Standard Fast':
        return M3EMotion.standardSpatialFast;
      case 'Expressive Fast':
      default:
        return M3EMotion.expressiveSpatialFast;
    }
  }

  void _log(String message) {
    setState(() {
      _lastEvent =
          '${DateTime.now().toIso8601String().substring(11, 19)} - $message';
    });
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final cs = theme.colorScheme;

    return Scaffold(
      appBar: AppBar(
        title: const Text('Material 3 Expressive Menus'),
        elevation: 1,
        actions: [
          IconButton(
            tooltip: widget.isDarkMode ? 'Switch to Light' : 'Switch to Dark',
            icon: Icon(
              widget.isDarkMode
                  ? Icons.light_mode_rounded
                  : Icons.dark_mode_rounded,
            ),
            onPressed: widget.onToggleTheme,
          ),
          // Showcase M3EPopupMenuButton in AppBar
          M3EPopupMenuButton<String>(
            tooltip: 'Overflow Options',
            icon: const Icon(Icons.more_vert_rounded),
            onSelected: (val) => _log('AppBar menu action: $val'),
            itemBuilder: (context) => [
              const M3EPopupMenuItem(
                value: 'settings',
                child: Row(
                  children: [
                    Icon(Icons.settings_outlined, size: 20),
                    SizedBox(width: 12),
                    Text('Settings'),
                  ],
                ),
              ),
              const M3EPopupMenuItem(
                value: 'about',
                child: Row(
                  children: [
                    Icon(Icons.info_outline_rounded, size: 20),
                    SizedBox(width: 12),
                    Text('About M3E Menu'),
                  ],
                ),
              ),
              const M3EPopupMenuDivider(),
              const M3EPopupMenuItem(
                value: 'logout',
                child: Row(
                  children: [
                    Icon(Icons.logout_rounded, size: 20, color: Colors.red),
                    SizedBox(width: 12),
                    Text('Sign out', style: TextStyle(color: Colors.red)),
                  ],
                ),
              ),
            ],
          ),
          const SizedBox(width: 8),
        ],
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 24.0),
        child: Center(
          child: ConstrainedBox(
            constraints: const BoxConstraints(maxWidth: 820),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                // Header Banner
                _buildHeader(cs),
                const SizedBox(height: 20),

                // Event Logger Card
                _buildEventLogger(cs),
                const SizedBox(height: 24),

                // Section 1: Standard & Cascading Submenu
                _buildCardSection(
                  title: '1. Cascading Menus & Submenus',
                  subtitle: 'Powered by Magic Triangle safe-hover navigation, spring physics motion, and submenus.',
                  child: Wrap(
                    spacing: 16,
                    runSpacing: 12,
                    alignment: WrapAlignment.start,
                    crossAxisAlignment: WrapCrossAlignment.center,
                    children: [
                      // File Menu with Cascading Submenus
                      M3EMenuAnchor(
                        controller: _fileMenuController,
                        menuChildren: [
                          M3EMenuGroup(
                            children: [
                              M3EMenuItemButton(
                                leadingIcon: const Icon(
                                  Icons.file_open_outlined,
                                  size: 20,
                                ),
                                trailingIcon: const Text(
                                  '⌘O',
                                  style: TextStyle(
                                    fontSize: 12,
                                    color: Colors.grey,
                                  ),
                                ),
                                onPressed: () => _log('File -> Open selected'),
                                child: const Text('Open File...'),
                              ),
                              M3EMenuItemButton(
                                leadingIcon: const Icon(
                                  Icons.save_outlined,
                                  size: 20,
                                ),
                                trailingIcon: const Text(
                                  '⌘S',
                                  style: TextStyle(
                                    fontSize: 12,
                                    color: Colors.grey,
                                  ),
                                ),
                                onPressed: () => _log('File -> Save selected'),
                                child: const Text('Save'),
                              ),
                              // Cascading Submenu 1
                              M3ESubmenuButton(
                                leadingIcon: const Icon(
                                  Icons.ios_share_outlined,
                                  size: 20,
                                ),
                                menuChildren: [
                                  M3EMenuItemButton(
                                    leadingIcon: const Icon(
                                      Icons.picture_as_pdf_outlined,
                                      size: 20,
                                    ),
                                    onPressed: () =>
                                        _log('Export -> PDF selected'),
                                    child: const Text('Export as PDF'),
                                  ),
                                  M3EMenuItemButton(
                                    leadingIcon: const Icon(
                                      Icons.image_outlined,
                                      size: 20,
                                    ),
                                    onPressed: () =>
                                        _log('Export -> PNG selected'),
                                    child: const Text('Export as PNG'),
                                  ),
                                  // Nested Level 2 Submenu
                                  M3ESubmenuButton(
                                    leadingIcon: const Icon(
                                      Icons.code_rounded,
                                      size: 20,
                                    ),
                                    menuChildren: [
                                      M3EMenuItemButton(
                                        onPressed: () => _log(
                                          'Export -> Code -> JSON selected',
                                        ),
                                        child: const Text('JSON Schema'),
                                      ),
                                      M3EMenuItemButton(
                                        onPressed: () => _log(
                                          'Export -> Code -> YAML selected',
                                        ),
                                        child: const Text('YAML Config'),
                                      ),
                                    ],
                                    child: const Text('Export as Code'),
                                  ),
                                ],
                                child: const Text('Export As'),
                              ),
                            ],
                          ),
                          const M3EMenuGroupLabel(child: Text('Danger Zone')),
                          M3EMenuGroup(
                            children: [
                              M3EMenuItemButton(
                                leadingIcon: const Icon(
                                  Icons.delete_outline_rounded,
                                  size: 20,
                                  color: Colors.red,
                                ),
                                onPressed: () => _log('Delete File selected'),
                                child: const Text(
                                  'Delete File',
                                  style: TextStyle(color: Colors.red),
                                ),
                              ),
                            ],
                          ),
                        ],
                        builder: (context, controller, child) {
                          return M3EButton(
                            style: M3EButtonStyle.filled,
                            onPressed: () {
                              if (controller.isOpen) {
                                controller.close();
                              } else {
                                controller.open();
                              }
                            },
                            child: const Row(
                              mainAxisSize: MainAxisSize.min,
                              children: [
                                Icon(Icons.folder_outlined, size: 18),
                                SizedBox(width: 8),
                                Text('File Actions (Cascading)'),
                                SizedBox(width: 4),
                                Icon(
                                  Icons.keyboard_arrow_down_rounded,
                                  size: 18,
                                ),
                              ],
                            ),
                          );
                        },
                      ),

                      // Standalone Submenu popup button
                      M3EButton(
                        style: M3EButtonStyle.tonal,
                        onPressed: () => _fileMenuController.isOpen
                            ? _fileMenuController.close()
                            : _fileMenuController.open(),
                        child: Text(
                          _fileMenuController.isOpen
                              ? 'Close Menu'
                              : 'Open Programmatically',
                        ),
                      ),
                    ],
                  ),
                ),
                const SizedBox(height: 20),

                // Section 2: Segmented Groups with Checkbox and Radio items
                _buildCardSection(
                  title: '2. Checkbox & Radio Menu Items',
                  subtitle: 'Segmented visual groups with morphing shapes, multi-select checkboxes, and single-select radios.',
                  child: M3EMenuAnchor(
                    controller: _optionsMenuController,
                    menuChildren: [
                      const M3EMenuGroupLabel(
                        child: Text('Display Preferences'),
                      ),
                      M3EMenuGroup(
                        children: [
                          M3ECheckboxMenuButton(
                            value: _showLineNumbers,
                            onChanged: (val) {
                              setState(() => _showLineNumbers = val ?? false);
                              _log('Line Numbers: $_showLineNumbers');
                            },
                            leadingIcon: const Icon(
                              Icons.format_list_numbered_rounded,
                              size: 20,
                            ),
                            child: const Text('Show line numbers'),
                          ),
                          M3ECheckboxMenuButton(
                            value: _autoSave,
                            onChanged: (val) {
                              setState(() => _autoSave = val ?? false);
                              _log('Auto Save: $_autoSave');
                            },
                            leadingIcon: const Icon(
                              Icons.sync_rounded,
                              size: 20,
                            ),
                            child: const Text('Auto-save changes'),
                          ),
                          M3ECheckboxMenuButton(
                            value: _wordWrap,
                            onChanged: (val) {
                              setState(() => _wordWrap = val ?? false);
                              _log('Word Wrap: $_wordWrap');
                            },
                            leadingIcon: const Icon(
                              Icons.wrap_text_rounded,
                              size: 20,
                            ),
                            child: const Text('Soft wrap lines'),
                          ),
                        ],
                      ),
                      const M3EMenuGroupLabel(child: Text('Layout Density')),
                      M3EMenuGroup(
                        children: [
                          M3ERadioMenuButton<String>(
                            value: 'Compact',
                            groupValue: _layoutDensity,
                            onChanged: (val) {
                              if (val != null) {
                                setState(() => _layoutDensity = val);
                                _log('Density set to: $val');
                              }
                            },
                            leadingIcon: const Icon(
                              Icons.density_small_rounded,
                              size: 20,
                            ),
                            child: const Text('Compact'),
                          ),
                          M3ERadioMenuButton<String>(
                            value: 'Comfortable',
                            groupValue: _layoutDensity,
                            onChanged: (val) {
                              if (val != null) {
                                setState(() => _layoutDensity = val);
                                _log('Density set to: $val');
                              }
                            },
                            leadingIcon: const Icon(
                              Icons.density_medium_rounded,
                              size: 20,
                            ),
                            child: const Text('Comfortable (Default)'),
                          ),
                          M3ERadioMenuButton<String>(
                            value: 'Spacious',
                            groupValue: _layoutDensity,
                            onChanged: (val) {
                              if (val != null) {
                                setState(() => _layoutDensity = val);
                                _log('Density set to: $val');
                              }
                            },
                            leadingIcon: const Icon(
                              Icons.density_large_rounded,
                              size: 20,
                            ),
                            child: const Text('Spacious'),
                          ),
                        ],
                      ),
                    ],
                    builder: (context, controller, child) {
                      return M3EButton(
                        style: M3EButtonStyle.outlined,
                        onPressed: () {
                          if (controller.isOpen) {
                            controller.close();
                          } else {
                            controller.open();
                          }
                        },
                        child: Row(
                          mainAxisSize: MainAxisSize.min,
                          children: [
                            const Icon(Icons.tune_rounded, size: 18),
                            const SizedBox(width: 8),
                            Text('Editor Settings ($_layoutDensity)'),
                            const SizedBox(width: 4),
                            const Icon(
                              Icons.keyboard_arrow_down_rounded,
                              size: 18,
                            ),
                          ],
                        ),
                      );
                    },
                  ),
                ),
                const SizedBox(height: 20),

                // Section 3: Interactive Playground / Configurable Style
                _buildCardSection(
                  title: '3. Interactive Style & Motion Playground',
                  subtitle: 'Test different spring physics, reveal animation styles, elevation, and anchor alignment.',
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.stretch,
                    children: [
                      Wrap(
                        spacing: 24,
                        runSpacing: 16,
                        children: [
                          // Position Selection
                          _buildDropdownSelector<M3EMenuPosition>(
                            label: 'Menu Position',
                            value: _selectedPosition,
                            items: const [
                              DropdownMenuItem(
                                value: M3EMenuPosition.under,
                                child: Text('under (Default)'),
                              ),
                              DropdownMenuItem(
                                value: M3EMenuPosition.over,
                                child: Text('over (Center)'),
                              ),
                            ],
                            onChanged: (val) {
                              if (val != null) {
                                setState(() => _selectedPosition = val);
                              }
                            },
                          ),

                          // Animation Style
                          _buildDropdownSelector<M3EMenuAnimationStyle>(
                            label: 'Animation Style',
                            value: _selectedAnimStyle,
                            items: const [
                              DropdownMenuItem(
                                value: M3EMenuAnimationStyle.popup,
                                child: Text('popup (Scale & Fade Elastic Pop)'),
                              ),
                              DropdownMenuItem(
                                value: M3EMenuAnimationStyle.dropdown,
                                child: Text(
                                  'dropdown (Vertical Reveal Unroll)',
                                ),
                              ),
                            ],
                            onChanged: (val) {
                              if (val != null) {
                                setState(() => _selectedAnimStyle = val);
                              }
                            },
                          ),

                          // Motion Preset
                          _buildDropdownSelector<String>(
                            label: 'Spring Motion Preset',
                            value: _motionPreset,
                            items: const [
                              DropdownMenuItem(
                                value: 'Expressive Fast',
                                child: Text('Expressive Fast'),
                              ),
                              DropdownMenuItem(
                                value: 'Expressive Slow',
                                child: Text('Expressive Slow'),
                              ),
                              DropdownMenuItem(
                                value: 'Standard Default',
                                child: Text('Standard Default'),
                              ),
                              DropdownMenuItem(
                                value: 'Standard Fast',
                                child: Text('Standard Fast'),
                              ),
                            ],
                            onChanged: (val) {
                              if (val != null) {
                                setState(() => _motionPreset = val);
                              }
                            },
                          ),
                        ],
                      ),
                      const SizedBox(height: 16),

                      // Elevation Slider
                      Row(
                        children: [
                          Text('Elevation (${_elevation.toStringAsFixed(1)}):'),
                          Expanded(
                            child: Slider(
                              value: _elevation,
                              min: 0,
                              max: 12,
                              divisions: 12,
                              onChanged: (val) =>
                                  setState(() => _elevation = val),
                            ),
                          ),
                        ],
                      ),
                      const SizedBox(height: 16),

                      // Launch configured menu button
                      Align(
                        alignment: Alignment.centerLeft,
                        child: M3EMenuAnchor(
                          controller: _styledMenuController,
                          style: M3EMenuStyle(
                            position: _selectedPosition,
                            animationStyle: _selectedAnimStyle,
                            spatialMotion: _currentMotion,
                            elevation: _elevation,
                          ),
                          menuChildren: [
                            M3EMenuGroup(
                              children: [
                                M3EMenuItemButton(
                                  leadingIcon: const Icon(
                                    Icons.rocket_launch_outlined,
                                    size: 20,
                                  ),
                                  onPressed: () =>
                                      _log('Playground: Launch clicked'),
                                  child: const Text('Launch Preview'),
                                ),
                                M3EMenuItemButton(
                                  leadingIcon: const Icon(
                                    Icons.refresh_rounded,
                                    size: 20,
                                  ),
                                  onPressed: () =>
                                      _log('Playground: Reset clicked'),
                                  child: const Text('Reset Settings'),
                                ),
                                M3EMenuItemButton(
                                  leadingIcon: const Icon(
                                    Icons.share_outlined,
                                    size: 20,
                                  ),
                                  onPressed: () =>
                                      _log('Playground: Share clicked'),
                                  child: const Text('Share Result'),
                                ),
                              ],
                            ),
                          ],
                          builder: (context, controller, child) {
                            return M3EButton(
                              style: M3EButtonStyle.elevated,
                              onPressed: () {
                                if (controller.isOpen) {
                                  controller.close();
                                } else {
                                  controller.open();
                                }
                              },
                              child: const Row(
                                mainAxisSize: MainAxisSize.min,
                                children: [
                                  Icon(Icons.play_arrow_rounded, size: 18),
                                  SizedBox(width: 8),
                                  Text('Open Customized Menu'),
                                ],
                              ),
                            );
                          },
                        ),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }

  Widget _buildHeader(ColorScheme cs) {
    return Container(
      padding: const EdgeInsets.all(20),
      decoration: BoxDecoration(
        color: cs.primaryContainer.withValues(alpha: 0.4),
        borderRadius: BorderRadius.circular(16),
        border: Border.all(color: cs.outlineVariant.withValues(alpha: 0.5)),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            children: [
              Icon(Icons.widgets_rounded, color: cs.primary, size: 28),
              const SizedBox(width: 12),
              Text(
                'M3E Menu Component Showcase',
                style: TextStyle(
                  fontSize: 20,
                  fontWeight: FontWeight.bold,
                  color: cs.onSurface,
                ),
              ),
            ],
          ),
          const SizedBox(height: 8),
          Text(
            'Explore Material 3 Expressive menus with spring-physics motion, '
            'Magic Triangle safe traversal, morphing group shapes, and check/radio items.',
            style: TextStyle(color: cs.onSurfaceVariant, fontSize: 14),
          ),
        ],
      ),
    );
  }

  Widget _buildEventLogger(ColorScheme cs) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
      decoration: BoxDecoration(
        color: cs.surfaceContainerHighest.withValues(alpha: 0.5),
        borderRadius: BorderRadius.circular(12),
        border: Border.all(color: cs.outlineVariant.withValues(alpha: 0.4)),
      ),
      child: Row(
        children: [
          Icon(Icons.terminal_rounded, size: 18, color: cs.primary),
          const SizedBox(width: 10),
          Text(
            'Latest Event: ',
            style: TextStyle(
              fontWeight: FontWeight.bold,
              fontSize: 13,
              color: cs.primary,
            ),
          ),
          Expanded(
            child: Text(
              _lastEvent,
              style: TextStyle(fontSize: 13, color: cs.onSurfaceVariant),
              overflow: TextOverflow.ellipsis,
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildCardSection({
    required String title,
    required String subtitle,
    required Widget child,
  }) {
    final cs = Theme.of(context).colorScheme;
    return Card(
      elevation: 0,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(16),
        side: BorderSide(color: cs.outlineVariant.withValues(alpha: 0.5)),
      ),
      child: Padding(
        padding: const EdgeInsets.all(20.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              title,
              style: TextStyle(
                fontSize: 16,
                fontWeight: FontWeight.bold,
                color: cs.onSurface,
              ),
            ),
            const SizedBox(height: 4),
            Text(
              subtitle,
              style: TextStyle(fontSize: 13, color: cs.onSurfaceVariant),
            ),
            const Divider(height: 24),
            child,
          ],
        ),
      ),
    );
  }

  Widget _buildDropdownSelector<T>({
    required String label,
    required T value,
    required List<DropdownMenuItem<T>> items,
    required ValueChanged<T?> onChanged,
  }) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          label,
          style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600),
        ),
        const SizedBox(height: 6),
        DropdownButton<T>(value: value, items: items, onChanged: onChanged),
      ],
    );
  }
}
0
likes
160
points
676
downloads

Documentation

API reference

Publisher

verified publishermuditpurohit.tech

Weekly Downloads

Material 3 Expressive menus, cascading submenus, segmented groups, and popup menus with spring physics.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter, m3e_buttons, material_ui, motor

More

Packages that depend on m3e_menu