cascading_menu 0.1.0 copy "cascading_menu: ^0.1.0" to clipboard
cascading_menu: ^0.1.0 copied to clipboard

A Flutter implementation of the cascading menu.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:avocado_utils/avocado_utils.dart' hide Theme, Listener;
import 'package:avocado_utils/avocado_utils.dart' as avocado;
import 'package:cascading_menu/cascading_menu.dart';

void main() {
  runApp(ExampleApp());
}

class ToogleBrightnessIntent extends Intent {
  const ToogleBrightnessIntent();
}

class ExampleApp extends StatefulWidget {
  ExampleApp({ Key? key }) : super(key: key);

  static const String title = 'Cascading Menu Example';

  static const toogleBrightnessActivator = SingleActivator(LogicalKeyboardKey.keyB, control: true);

  @override
  State<ExampleApp> createState() => _ExampleAppState();
}

class _ExampleAppState extends State<ExampleApp> {
  ThemeMode themeMode = ThemeMode.dark;
  VisualDensity visualDensity = VisualDensity.adaptivePlatformDensity;

  ThemeData makeTheme(ColorScheme colorScheme) {
    return avocado.themeFromColorScheme(colorScheme).copyWith(
      visualDensity: visualDensity
    );
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: ExampleApp.title,
      theme: makeTheme(
        const ColorScheme.light(surface: Color(0xFFF0F0F0))
      ), 
      darkTheme: makeTheme(
        const ColorScheme.dark(),
      ),
      themeMode: themeMode,
      builder: (context, child) {
        final theme = Theme.of(context);
        
        return avocado.Theme(
          child: MenuTheme(
            style: MenuStyle(
              itemStyle: MenuItemStyle(
                overlayColor: MenuItemMaterialStateProperty.resolveWith((states) {
                  return theme.colorScheme.primary.withOpacity(0.5);
                }),
                startPaddingStyle: StartPaddingStyle.alwaysLargeForVerticalMenuWithAtLeastOneLeadingWidget
              ),
            ),
            child: child!
          ),
        );
      },
      home: Example(),
      shortcuts: {
        ...WidgetsApp.defaultShortcuts,
        ExampleApp.toogleBrightnessActivator: const ToogleBrightnessIntent(),
      },
      actions: {
        ...WidgetsApp.defaultActions,
        ToogleBrightnessIntent : 
          CallbackAction(onInvoke: (_) => setState(() {
            assert(themeMode == ThemeMode.light || themeMode == ThemeMode.dark);
            if (themeMode == ThemeMode.dark)
                 themeMode = ThemeMode.light;
            else themeMode = ThemeMode.dark;
          }))
      },
      debugShowCheckedModeBanner: false,
    );
  }
}

enum ExampleColor {
  none, red, yellow, green, blue 
}

class FillIntent extends Intent {
  const FillIntent(this.color);
  final ExampleColor color;
}

class MoveIntent extends Intent {
  const MoveIntent(this.direction);
  final VerticalDirection direction;
}

class Example extends StatefulWidget {
  static const lightColors = {
    ExampleColor.red: Color(0xFFf48686),
    ExampleColor.yellow: Color(0xFFfaf666),
    ExampleColor.green: Color(0XFF09e8ae),
    ExampleColor.blue: Color(0XFF87ccf5)
  }; 

  static const darkColors = {
    ExampleColor.red: Color(0xFF703232),
    ExampleColor.yellow: Color(0xFF76731d),
    ExampleColor.green: Color(0xFF00644a),
    ExampleColor.blue: Color(0xFF355b71)
  };

  static Color? resolveColor(ExampleColor color, Brightness brightness) {
    if (color == ExampleColor.none) return null;
    else return brightness == Brightness.light ? 
      Example.lightColors[color] : Example.darkColors[color];
  }

  static const moveUpActivator   = SingleActivator(LogicalKeyboardKey.arrowUp, alt: true);
  static const moveDownActivator = SingleActivator(LogicalKeyboardKey.arrowDown, alt: true);
  static const doNothingActivator0 = SingleActivator(LogicalKeyboardKey.keyA, control: true);
  static const doNothingActivator1 = SingleActivator(LogicalKeyboardKey.keyR, control: true);
  static const doNothingActivator2 = SingleActivator(LogicalKeyboardKey.keyT, alt: true);

  @override
  State<Example> createState() => _ExampleState();
}

class _ExampleState extends State<Example> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: _buildAppBar(context),
      body: _buildBody(context)
    );
  }

  AppBar _buildAppBar(BuildContext context) {
    final theme = Theme.of(context);
    final colorScheme = theme.colorScheme;
    final double toolbarHeight = barHeight(theme.visualDensity);

    return AppBar(
      toolbarHeight: toolbarHeight,
      leadingWidth: toolbarHeight,
      title: const Text(ExampleApp.title),
      actions: [
        Center(
          child: MenuButton(
            menu: Menu(
              items: [
                MenuItem(
                  title: const Text('Toogle Brightness'),
                  shortcutActivator: ExampleApp.toogleBrightnessActivator,
                ),
                MenuItem(
                  title: const Text('Interea Venientum'),
                  onPressed: () {}
                ),
              ]
            )
          ),
        ),
      ],
      elevation: kMenuElevation,
      backgroundColor: colorScheme.surface,
      foregroundColor: theme.brightness == Brightness.light ? 
        colorScheme.onSurface.withOpacity(0.55) : colorScheme.onSurface,
    );
  } 

  List<Widget> regions = [
    ExampleRegion(
      'Region with vertical menu',
      key: GlobalKey(),
      menuAxis: Axis.vertical,
    ),
    ExampleRegion(
      'Region with horizontal menu',
      key: GlobalKey(),
      menuAxis: Axis.horizontal,
    ),
    ExampleRegion(
      'Region with centered menu',
      key: GlobalKey(),
      menuAxis: Axis.vertical,
      menuAlignment: Alignment.center
    ),
  ];

  Widget _buildBody(BuildContext context) {
    return Shortcuts(
      shortcuts: {
        Example.moveUpActivator:      const MoveIntent(VerticalDirection.up),  
        Example.moveDownActivator:    const MoveIntent(VerticalDirection.down),

        Example.doNothingActivator0:  DoNothingIntent(),
        Example.doNothingActivator1:  DoNothingIntent(),
        Example.doNothingActivator2:  DoNothingIntent(),
      },
      child: SingleChildScrollView(
        child: Center(
          child: Padding(
            padding: const EdgeInsets.symmetric(horizontal: kPadding05),
            child: SizedBox(
              width: kUnit * 64,
              child: Column(
                children: regions.mapIndexed((region, i) {
                  return Actions(
                    actions: {
                      MoveIntent : CallbackAction<MoveIntent>(onInvoke: (intent) {
                        int? j;
                        switch (intent.direction) {
                          case VerticalDirection.up:
                            if (i > 0) j = i-1;
                            break;
                          case VerticalDirection.down:
                            if (i < (regions.length - 1)) j = i+1;
                            break;
                        }

                        if (j != null) 
                          setState(() => regions.swap(i, j!));

                        return;
                      })
                    },
                    child: Padding(
                      padding: EdgeInsets.only(
                        top: kPadding05, 
                        bottom: region != regions.last ? 0 : kPadding
                      ),
                      child: region,
                    ),
                  );
                })
              )
            ),
          ),
        )
      )
    );
  }
}

class ExampleRegion extends StatefulWidget {
  const ExampleRegion(
    this.text, {
    Key? key,
    required this.menuAxis,
    this.menuAlignment
  }) : super(key: key);

  static const double kMinFontSizeLimit = 15.0;
  static const double kFontSize = 20.0;
  static const double kMaxFontSizeLimit = 25.0;
  static const double kMinHeight = kFontSize + 2 * kPadding + kUnit * 8;

  final String text;
  final Axis menuAxis;
  final Alignment? menuAlignment;

  @override
  State<ExampleRegion> createState() => _ExampleRegionState();
}

class _ExampleRegionState extends State<ExampleRegion> {
  final focusNode = FocusNode(); 
  ExampleColor color = ExampleColor.none;
  double fontSize = ExampleRegion.kFontSize;
  static const kFontWeights = [ FontWeight.w300, FontWeight.w400, FontWeight.w600 ];
  int fontWeightIndex = 1;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final borderWidth = toLogicalPixels(1, context);
    final headline5 = theme.textTheme.headline5!;

    return ContextMenuRegion(
      menuAxis: widget.menuAxis,
      menuAlignment: widget.menuAlignment,
      menu: buildMenu(widget.menuAxis),
      child: Listener(
        onPointerDown: (_) => focusNode.requestFocus(),
        child: Focus(
          focusNode: focusNode,
          onFocusChange: (focused) => setState(() {}),
          child: Container(
            alignment: Alignment.center,
            padding: const EdgeInsets.all(kPadding),
            decoration: BoxDecoration(
              color: Example.resolveColor(color, theme.brightness),
              border: focusNode.hasPrimaryFocus ?
                Border.all(
                  color: theme.brightness == Brightness.light ? 
                    Colors.black : Colors.blue.shade300,
                  width: borderWidth * 3
                ) :
                Border.all(
                  color: theme.brightness == Brightness.light ? 
                    Colors.black.withOpacity(0.3) : Colors.white.withOpacity(0.5),
                  width: borderWidth,
                  style: color == ExampleColor.none ? BorderStyle.solid : BorderStyle.none
                ),
              borderRadius: const BorderRadius.all(Radius.circular(kRadius))  
            ),
            constraints: const BoxConstraints(minHeight: ExampleRegion.kMinHeight),
            child: Text(
              widget.text, 
              style: headline5.copyWith(
                color: headline5.color?.withOpacity(0.3),
                fontSize: fontSize,
                fontWeight: kFontWeights[fontWeightIndex]
              ),
            ),
          ),
        ),
      ),
    );
  }

  Menu buildMenu(Axis axis) {
    void changeColor(ExampleColor? value) => setState(() => color = value!);

    return Menu(
      items: [
        MenuItem(
          title: const Text('Ad Astra'), 
          enabled: color != ExampleColor.none,
          shortcutActivator: Example.doNothingActivator0,
        ),
        
        if (axis == Axis.vertical)
          MenuItem(
            title: const Text('Haec Concordia'), 
            onPressed: color == ExampleColor.none ? null : () {}
          ),

        MenuItem(
          title: const Text('Arrange'),
          submenu: Menu(
            items: [
              MenuItem(
                title: const Text('Move Up'),
                shortcutActivator: Example.moveUpActivator,
              ),

              MenuItem(
                title: const Text('Move Down'),
                shortcutActivator: Example.moveDownActivator,
              ),
            ]
          ),
        ), 

        MenuItem(
          leading: const Icon(Icons.star_outlined),
          title: const Text('Appearance'),
          submenu: Menu(
            items: [
              MenuItem(
                title: const Text('Fill'),
                leading: const Icon(Icons.format_color_fill_outlined),
                submenu: Menu(
                  menuStyle: MenuStyle(
                    vertMenuMinWidth: kVertMenuMinWidth * 0.75
                  ),
                  items: [
                    MenuRadioGroup<ExampleColor>(
                      values : const {
                        ExampleColor.none   : 'None',
                        ExampleColor.red    : 'Red',
                        ExampleColor.yellow : 'Yellow',
                        ExampleColor.green  : 'Green',
                        ExampleColor.blue   : 'Blue'                      
                      },
                      groupValue: color,
                      onChanged: changeColor,
                      controlAffinity: MenuItemControlAffinity.trailing,
                    )
                  ]
                ),
              ),

              MenuItem(
                leading: const Icon(Icons.text_format_outlined),
                title: const Text('Font'), 
                submenu: Menu(
                  items: [
                    SliderMenuItem(
                      title: const Text('Size'), 
                      value: fontSize, 
                      onChanged: (value) => setState(() => fontSize = value),
                      min: ExampleRegion.kMinFontSizeLimit,
                      max: ExampleRegion.kMaxFontSizeLimit,
                      divisions: 10,
                    ),
                    SliderMenuItem(
                      title: const Text('Weight'), 
                      value: fontWeightIndex.toDouble(), 
                      onChanged: (value) => setState(() => fontWeightIndex = value.toInt()),
                      min: 0.0,
                      max: 2.0,
                      divisions: 2,
                      label: kFontWeights[fontWeightIndex].toString().split('.').last,
                    ),
                  ],
                ),
              ),
              
            ]
          ),
        ),

        const MenuDivider(),

        MenuItem(
          leading: const Icon(Icons.emoji_emotions), 
          title: const Text('Conformeu'),
          shortcutActivator: Example.doNothingActivator1,
        ),

        const MenuDivider(),

        if (axis == Axis.vertical)
          MenuItem(
            title: const Text('Interea Venientum'),
            onPressed: () {}
          ),

        if (axis == Axis.vertical) 
          MenuItem(
            leading: const Icon(Icons.assessment),
            title: const Text('Tellure Levatur'), 
            shortcutActivator: Example.doNothingActivator2,
            enabled:  color != ExampleColor.none,
          ),
      ]
    );
  }
}
17
likes
70
pub points
2%
popularity

Publisher

unverified uploader

A Flutter implementation of the cascading menu.

Repository (GitHub)
View/report issues

Documentation

API reference

License

BSD-3-Clause (LICENSE)

Dependencies

avocado_utils, flutter

More

Packages that depend on cascading_menu