combos 1.0.0 copy "combos: ^1.0.0" to clipboard
combos: ^1.0.0 copied to clipboard

outdated

Combo Widgets for Flutter.

example/lib/main.dart

import 'dart:math' as math;

import 'package:combos/combos.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';

const _customAnimationDurationMs = 150;

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) => MaterialApp(
        title: 'Combo Samples',
        home: MyHomePage(),
      );
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final _comboKey = GlobalKey<ComboState>();
  final _awaitComboKey = GlobalKey<ComboState>();
  GlobalKey<_TestPopupState> _popupKey;
  GlobalKey<_TestPopupState> _awaitPopupKey;
  final _offsetXController = TextEditingController(text: '0');
  final _offsetYController = TextEditingController(text: '0');
  final _screenPaddingHorizontalController = TextEditingController(text: '0');
  final _screenPaddingVerticalController = TextEditingController(text: '0');
  final _animationDurationController = TextEditingController(text: '150');
  final _requiredSpaceController = TextEditingController(text: '300');
  final _popupWidthController = TextEditingController(text: '300');
  final _itemsCountController = TextEditingController(text: '3');
  final _spaceAboveController = TextEditingController(text: '16');
  final _comboWidthController = TextEditingController(text: '200');

  var _position = PopupPosition.bottomMinMatch;
  var _offsetX = 0;
  var _offsetY = 0;
  var _screenPaddingHorizontal = 0;
  var _screenPaddingVertical = 0;
  var _requiredSpace = 300;
  var _autoClose = PopupAutoClose.tapOutsideWithChildIgnorePointer;
  var _autoOpen = PopupAutoOpen.tap;
  var _animation = PopupAnimation.fade;
  var _animationDurationMs = 150;
  var _popupWidth = 300;
  var _itemsCount = 3;
  var _spaceAbove = 16;
  var _comboWidth = 200;
  var _comboAlignment = CrossAxisAlignment.center;

  String _selectorItem;
  String _typeaheadItem;

  @override
  Widget build(BuildContext context) {
    Widget buildEnumSelector<T>(String title, Iterable<T> values, T value,
            void Function(T value) setValue) =>
        Row(mainAxisSize: MainAxisSize.min, children: [
          Text(title,
              style:
                  TextStyle(color: Colors.grey, fontWeight: FontWeight.bold)),
          const SizedBox(width: 8),
          DropdownButton<T>(
            items: values
                .map((_) => DropdownMenuItem<T>(
                    value: _, child: Text(TextHelper.enumToString(_))))
                .toList(),
            value: value,
            onChanged: (_) => setState(() => setValue(_)),
          ),
        ]);

    Widget buildBoolSelector(
            String title, bool value, void Function(bool value) setValue) =>
        SizedBox(
          width: 200,
          child: CheckboxListTile(
            value: value,
            onChanged: (_) => setState(() => setValue(_)),
            controlAffinity: ListTileControlAffinity.leading,
            title: Text(title),
            dense: true,
          ),
        );

    Widget buildIntSelector(TextEditingController controller, String labelText,
            void Function(int value) setValue,
            [enabled = true]) =>
        SizedBox(
          width: 200,
          child: TextField(
            controller: controller,
            enabled: enabled,
            inputFormatters: [
              IntTextInputFormatter(minValue: 0, maxValue: 5000)
            ],
            decoration: InputDecoration(labelText: labelText),
            onChanged: (_) => setState(() => setValue(int.tryParse(_) ?? 0)),
          ),
        );

    Widget buildComboContainer({String title, Widget child}) =>
        Row(mainAxisSize: MainAxisSize.min, children: [
          SizedBox(
              width: 160,
              child: Text(
                title,
                style: const TextStyle(
                    color: Colors.grey, fontWeight: FontWeight.bold),
              )),
          child,
        ]);

    return Scaffold(
      appBar: AppBar(
        title: Text('Combo Samples'),
      ),
      body: ListView(
        children: [
          Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
            // properties
            Padding(
              padding: const EdgeInsets.all(16.0),
              child: Wrap(spacing: 24, runSpacing: 16, children: [
                // position
                buildEnumSelector<PopupPosition>('Position:',
                    PopupPosition.values, _position, (_) => _position = _),

                // offsetX
                buildIntSelector(
                    _offsetXController, 'Offset X', (_) => _offsetX = _),

                // offsetY
                buildIntSelector(
                    _offsetYController, 'Offset Y', (_) => _offsetY = _),

                // screen padding horizontal
                buildIntSelector(
                    _screenPaddingHorizontalController,
                    'Screen Padding Horizontal',
                    (_) => _screenPaddingHorizontal = _),

                // screen padding vertical
                buildIntSelector(
                    _screenPaddingVerticalController,
                    'Screen Padding Horizontal',
                    (_) => _screenPaddingVertical = _),

                // requiredSpace
                buildIntSelector(_requiredSpaceController, 'Required Space',
                    (_) => _requiredSpace = _),

                // auto close
                buildEnumSelector<PopupAutoClose>('Auto Close:',
                    PopupAutoClose.values, _autoClose, (_) => _autoClose = _),

                // auto open
                buildEnumSelector<PopupAutoOpen>('Auto Open:',
                    PopupAutoOpen.values, _autoOpen, (_) => _autoOpen = _),

                // animation
                buildEnumSelector<PopupAnimation>('Animation:',
                    PopupAnimation.values, _animation, (_) => _animation = _),

                // animationDuration
                buildIntSelector(
                    _animationDurationController,
                    'Animation Duration (Ms)',
                    (_) => _animationDurationMs = _,
                    _animation != PopupAnimation.custom &&
                        _animation != PopupAnimation.none),

                // popup width
                buildIntSelector(_popupWidthController, 'Popup Width',
                    (_) => _popupWidth = _),

                // items count
                buildIntSelector(_itemsCountController, 'Items Count',
                    (_) => _itemsCount = _),

                // space above
                buildIntSelector(_spaceAboveController, 'Space Above',
                    (_) => _spaceAbove = _),

                // space above
                buildIntSelector(_comboWidthController, 'Combo Width',
                    (_) => _comboWidth = _),

                // horizontalBehavior
                buildEnumSelector<CrossAxisAlignment>(
                    'Combo Alignment:',
                    [
                      CrossAxisAlignment.start,
                      CrossAxisAlignment.center,
                      CrossAxisAlignment.end
                    ],
                    _comboAlignment,
                    (_) => _comboAlignment = _),

                // close
                SizedBox(
                  width: 200,
                  child: RaisedButton(
                      color: Colors.blueAccent,
                      textColor: Colors.white,
                      child: Text('Close Popup'),
                      onPressed: Combo.closeAll),
                )
              ]),
            ),

            // combos
            Padding(
              padding: const EdgeInsets.symmetric(horizontal: 24),
              child: Column(crossAxisAlignment: _comboAlignment, children: [
                // space
                SizedBox(
                    key: ValueKey(_spaceAbove), height: _spaceAbove.toDouble()),

                // Combo
                buildComboContainer(
                  title: 'Combo',
                  child: Material(
                    borderRadius: BorderRadius.circular(29),
                    child: Combo(
                      key: _comboKey,
                      position: _position,
                      offset: Offset(_offsetX.toDouble(), _offsetY.toDouble()),
                      requiredSpace: _requiredSpace.toDouble(),
                      screenPadding: EdgeInsets.symmetric(
                          horizontal: _screenPaddingHorizontal.toDouble(),
                          vertical: _screenPaddingVertical.toDouble()),
                      autoClose: _autoClose,
                      autoOpen: _autoOpen,
                      animation: _animation,
                      animationDuration:
                          Duration(milliseconds: _animationDurationMs),
                      openedChanged: (isOpened) {
                        if (!isOpened && _animation == PopupAnimation.custom) {
                          _popupKey.currentState?.animatedClose();
                        }
                      },
                      child: Container(
                        width: _comboWidth.toDouble(),
                        height: 58,
                        decoration: BoxDecoration(
                          gradient: LinearGradient(
                              begin: Alignment.topCenter,
                              end: Alignment.bottomCenter,
                              colors: [
                                Colors.blueAccent.withOpacity(0.2),
                                Colors.blueAccent.withOpacity(0.0),
                                Colors.blueAccent.withOpacity(0.0),
                                Colors.blueAccent.withOpacity(0.2),
                              ]),
                          borderRadius: BorderRadius.circular(29),
                          border: Border.all(color: Colors.blueAccent),
                        ),
                        child: Row(
                          children: [
                            const Expanded(
                                child: Text('Combo Child',
                                    textAlign: TextAlign.center,
                                    style:
                                        TextStyle(color: Colors.blueAccent))),
                            const Padding(
                              padding: EdgeInsets.symmetric(horizontal: 16),
                              child: Icon(Icons.arrow_drop_down,
                                  color: Colors.blueAccent),
                            ),
                          ],
                        ),
                      ),
                      popupBuilder: (context, mirrored) => TestPopup(
                          key: _popupKey = GlobalKey<_TestPopupState>(),
                          mirrored: mirrored,
                          width: _popupWidth.toDouble(),
                          itemsCount: _itemsCount,
                          onClose: () => _comboKey.currentState?.close(),
                          animated: _animation == PopupAnimation.custom,
                          radius: const Radius.circular(24)),
                      highlightColor: Colors.blueAccent.withOpacity(0.1),
                      splashColor: Colors.blueAccent.withOpacity(0.1),
                      hoverColor: Colors.blueAccent.withOpacity(0.1),
                    ),
                  ),
                ),

                const SizedBox(height: 16),

                // AwaitCombo
                buildComboContainer(
                  title: 'AwaitCombo',
                  child: Material(
                    borderRadius: BorderRadius.circular(29),
                    child: Builder(builder: (context) {
                      final horizontal = _position == PopupPosition.left ||
                          _position == PopupPosition.right;
                      return AwaitCombo(
                        key: _awaitComboKey,
                        refreshOnOpened: true,
                        progressPosition: horizontal
                            ? ProgressPosition.child
                            : ProgressPosition.popup,
                        progressDecoratorBuilder:
                            (context, waiting, mirrored, child) =>
                                ProgressDecorator(
                          waiting: waiting,
                          mirrored: mirrored,
                          child: child,
                          progressBackgroundColor:
                              horizontal ? Colors.transparent : null,
                          progressValueColor: horizontal
                              ? AlwaysStoppedAnimation(
                                  Colors.blueAccent.withOpacity(0.2))
                              : null,
                          progressHeight: horizontal ? null : 2.0,
                        ),
                        position: _position,
                        offset:
                            Offset(_offsetX.toDouble(), _offsetY.toDouble()),
                        requiredSpace: _requiredSpace.toDouble(),
                        screenPadding: EdgeInsets.symmetric(
                            horizontal: _screenPaddingHorizontal.toDouble(),
                            vertical: _screenPaddingVertical.toDouble()),
                        autoClose: _autoClose,
                        autoOpen: _autoOpen,
                        animation: _animation,
                        animationDuration:
                            Duration(milliseconds: _animationDurationMs),
                        openedChanged: (isOpened) {
                          if (!isOpened &&
                              _animation == PopupAnimation.custom) {
                            _popupKey.currentState?.animatedClose();
                          }
                        },
                        child: Container(
                          width: _comboWidth.toDouble(),
                          height: 58,
                          decoration: BoxDecoration(
                            gradient: LinearGradient(
                                begin: Alignment.topCenter,
                                end: Alignment.bottomCenter,
                                colors: [
                                  Colors.blueAccent.withOpacity(0.2),
                                  Colors.blueAccent.withOpacity(0.0),
                                  Colors.blueAccent.withOpacity(0.0),
                                  Colors.blueAccent.withOpacity(0.2),
                                ]),
                            borderRadius: BorderRadius.circular(29),
                            border: Border.all(color: Colors.blueAccent),
                          ),
                          child: Row(
                            children: [
                              const Expanded(
                                  child: Text('AwaitCombo Child',
                                      textAlign: TextAlign.center,
                                      style:
                                          TextStyle(color: Colors.blueAccent))),
                              const Padding(
                                padding: EdgeInsets.symmetric(horizontal: 16),
                                child: Icon(Icons.arrow_drop_down,
                                    color: Colors.blueAccent),
                              ),
                            ],
                          ),
                        ),
                        popupBuilder: (context) async {
                          await Future.delayed(Duration(milliseconds: 500));
                          return TestPopup(
                              key: _awaitPopupKey =
                                  GlobalKey<_TestPopupState>(),
                              mirrored: false,
                              width: _popupWidth.toDouble(),
                              itemsCount: _itemsCount,
                              onClose: () => _comboKey.currentState?.close(),
                              animated: _animation == PopupAnimation.custom,
                              radius: const Radius.circular(24));
                        },
                        highlightColor: Colors.blueAccent.withOpacity(0.1),
                        splashColor: Colors.blueAccent.withOpacity(0.1),
                        hoverColor: Colors.blueAccent.withOpacity(0.1),
                      );
                    }),
                  ),
                ),

                const SizedBox(height: 16),

                // ListCombo
                buildComboContainer(
                  title: 'ListCombo',
                  child: Material(
                    borderRadius: BorderRadius.circular(29),
                    child: Builder(builder: (context) {
                      final horizontal = _position == PopupPosition.left ||
                          _position == PopupPosition.right;
                      return ListCombo<String>(
                        getList: () async {
                          await Future.delayed(Duration(milliseconds: 500));
                          return Iterable.generate(_itemsCount)
                              .map((_) => 'Item ${_ + 1}')
                              .toList();
                        },
                        itemBuilder: (context, item) =>
                            ListTile(title: Text(item)),
                        onItemTapped: (item) {
                          final dialog =
                              AlertDialog(content: Text('$item tapped!'));
                          showDialog(context: context, builder: (_) => dialog);
                        },
                        popupBuilder: _position == PopupPosition.bottomMatch ||
                                _position == PopupPosition.topMatch
                            ? null
                            : (context, list, itemBuilder, onItemTapped,
                                    mirrored, getIsSelectable) =>
                                ListPopup<String>(
                                    list: list,
                                    itemBuilder: itemBuilder,
                                    onItemTapped: onItemTapped,
                                    width: 300,
                                    getIsSelectable: getIsSelectable),
                        refreshOnOpened: true,
                        progressPosition: horizontal
                            ? ProgressPosition.child
                            : ProgressPosition.popup,
                        progressDecoratorBuilder:
                            (context, waiting, mirrored, child) =>
                                ProgressDecorator(
                          waiting: waiting,
                          mirrored: mirrored,
                          child: child,
                          progressBackgroundColor:
                              horizontal ? Colors.transparent : null,
                          progressValueColor: horizontal
                              ? AlwaysStoppedAnimation(
                                  Colors.blueAccent.withOpacity(0.2))
                              : null,
                          progressHeight: horizontal ? null : 2.0,
                        ),
                        position: _position,
                        offset:
                            Offset(_offsetX.toDouble(), _offsetY.toDouble()),
                        requiredSpace: _requiredSpace.toDouble(),
                        screenPadding: EdgeInsets.symmetric(
                            horizontal: _screenPaddingHorizontal.toDouble(),
                            vertical: _screenPaddingVertical.toDouble()),
                        autoClose: _autoClose,
                        autoOpen: _autoOpen,
                        animation: _animation,
                        animationDuration:
                            Duration(milliseconds: _animationDurationMs),
                        openedChanged: (isOpened) {
                          if (!isOpened &&
                              _animation == PopupAnimation.custom) {
                            _popupKey.currentState?.animatedClose();
                          }
                        },
                        child: Container(
                          width: _comboWidth.toDouble(),
                          height: 58,
                          decoration: BoxDecoration(
                            gradient: LinearGradient(
                                begin: Alignment.topCenter,
                                end: Alignment.bottomCenter,
                                colors: [
                                  Colors.blueAccent.withOpacity(0.2),
                                  Colors.blueAccent.withOpacity(0.0),
                                  Colors.blueAccent.withOpacity(0.0),
                                  Colors.blueAccent.withOpacity(0.2),
                                ]),
                            borderRadius: BorderRadius.circular(29),
                            border: Border.all(color: Colors.blueAccent),
                          ),
                          child: Row(
                            children: [
                              const Expanded(
                                  child: Text('ListCombo Child',
                                      textAlign: TextAlign.center,
                                      style:
                                          TextStyle(color: Colors.blueAccent))),
                              const Padding(
                                padding: EdgeInsets.symmetric(horizontal: 16),
                                child: Icon(Icons.arrow_drop_down,
                                    color: Colors.blueAccent),
                              ),
                            ],
                          ),
                        ),
                        highlightColor: Colors.blueAccent.withOpacity(0.1),
                        splashColor: Colors.blueAccent.withOpacity(0.1),
                        hoverColor: Colors.blueAccent.withOpacity(0.1),
                      );
                    }),
                  ),
                ),

                const SizedBox(height: 16),

                // ListCombo
                buildComboContainer(
                  title: 'SelectorCombo',
                  child: Container(
                    width: _comboWidth.toDouble(),
                    height: 58,
                    decoration: BoxDecoration(
                      gradient: LinearGradient(
                          begin: Alignment.topCenter,
                          end: Alignment.bottomCenter,
                          colors: [
                            Colors.blueAccent.withOpacity(0.2),
                            Colors.blueAccent.withOpacity(0.0),
                            Colors.blueAccent.withOpacity(0.0),
                            Colors.blueAccent.withOpacity(0.2),
                          ]),
                      borderRadius: BorderRadius.circular(29),
                      border: Border.all(color: Colors.blueAccent),
                    ),
                    child: Material(
                      color: Colors.transparent,
                      borderRadius: BorderRadius.circular(29),
                      child: Builder(builder: (context) {
                        final horizontal = _position == PopupPosition.left ||
                            _position == PopupPosition.right;
                        return SelectorCombo<String>(
                          selected: _selectorItem,
                          childBuilder: (context, item) => item == null
                              ? const Center(
                                  child: Text('<None>',
                                      style: TextStyle(color: Colors.grey)),
                                )
                              : ListTile(
                                  title: Text(item,
                                      style: const TextStyle(
                                          color: Colors.blueAccent)),
                                ),
                          getList: () async {
                            await Future.delayed(Duration(milliseconds: 500));
                            return Iterable.generate(_itemsCount)
                                .map((_) => 'Item ${_ + 1}')
                                .toList();
                          },
                          itemBuilder: (context, item) =>
                              ListTile(title: Text(item ?? '')),
                          onItemTapped: (item) =>
                              setState(() => _selectorItem = item),
                          popupBuilder:
                              _position == PopupPosition.bottomMatch ||
                                      _position == PopupPosition.topMatch
                                  ? null
                                  : (context, list, itemBuilder, onItemTapped,
                                          mirrored, getIsSelectable) =>
                                      ListPopup<String>(
                                          list: list,
                                          itemBuilder: itemBuilder,
                                          onItemTapped: onItemTapped,
                                          width: 300,
                                          getIsSelectable: getIsSelectable),
                          refreshOnOpened: true,
                          progressPosition: horizontal
                              ? ProgressPosition.child
                              : ProgressPosition.popup,
                          progressDecoratorBuilder:
                              (context, waiting, mirrored, child) =>
                                  ProgressDecorator(
                            waiting: waiting,
                            mirrored: mirrored,
                            child: child,
                            progressBackgroundColor:
                                horizontal ? Colors.transparent : null,
                            progressValueColor: horizontal
                                ? AlwaysStoppedAnimation(
                                    Colors.blueAccent.withOpacity(0.2))
                                : null,
                            progressHeight: horizontal ? null : 2.0,
                          ),
                          position: _position,
                          offset:
                              Offset(_offsetX.toDouble(), _offsetY.toDouble()),
                          requiredSpace: _requiredSpace.toDouble(),
                          screenPadding: EdgeInsets.symmetric(
                              horizontal: _screenPaddingHorizontal.toDouble(),
                              vertical: _screenPaddingVertical.toDouble()),
                          autoClose: _autoClose,
                          autoOpen: _autoOpen,
                          animation: _animation,
                          animationDuration:
                              Duration(milliseconds: _animationDurationMs),
                          openedChanged: (isOpened) {
                            if (!isOpened &&
                                _animation == PopupAnimation.custom) {
                              _popupKey.currentState?.animatedClose();
                            }
                          },
                          highlightColor: Colors.blueAccent.withOpacity(0.1),
                          splashColor: Colors.blueAccent.withOpacity(0.1),
                          hoverColor: Colors.blueAccent.withOpacity(0.1),
                        );
                      }),
                    ),
                  ),
                ),

                const SizedBox(height: 16),

                // TypeaheadCombo
                buildComboContainer(
                  title: 'TypeaheadCombo',
                  child: Container(
                    width: _comboWidth.toDouble(),
                    decoration: BoxDecoration(
                      borderRadius: BorderRadius.circular(29),
                      gradient: LinearGradient(
                          begin: Alignment.topCenter,
                          end: Alignment.bottomCenter,
                          colors: [
                            Colors.blueAccent.withOpacity(0.2),
                            Colors.blueAccent.withOpacity(0.0),
                            Colors.blueAccent.withOpacity(0.0),
                            Colors.blueAccent.withOpacity(0.2),
                          ]),
                    ),
                    child: Builder(builder: (context) {
                      final horizontal = _position == PopupPosition.left ||
                          _position == PopupPosition.right;
                      return TypeaheadCombo<String>(
                        selected: _typeaheadItem,
                        onItemTapped: (_) => setState(() => _typeaheadItem = _),
                        getList: (text) async {
                          await Future.delayed(Duration(seconds: 1));
                          return [
                            'Item1',
                            'Item2',
                            'Item3',
                            'Item4',
                            'Item5',
                            'Item6',
                            'Item7',
                          ];
                        },
                        itemBuilder: (context, item) => ListTile(
                            selected: item == _typeaheadItem,
                            title: Text(item)),
                        getItemText: (item) => item,
                        //minTextLength: 0,
                        //popupWidth: _popupWidth.toDouble(),
                        //popupMaxHeight: _popupHeight.toDouble(),
                        decoration: InputDecoration(
                          labelText: 'Sample Typeahead',
                          enabledBorder: OutlineInputBorder(
                            borderRadius: BorderRadius.circular(29),
                            borderSide: BorderSide(color: Colors.blueAccent),
                          ),
                          border: OutlineInputBorder(
                            borderRadius: BorderRadius.circular(29),
                            borderSide: BorderSide(color: Colors.blueAccent),
                          ),
                          labelStyle: TextStyle(color: Colors.blueAccent),
                        ),
                        popupBuilder: _position == PopupPosition.bottomMatch ||
                                _position == PopupPosition.topMatch
                            ? null
                            : (context, list, itemBuilder, onItemTapped,
                                    mirrored, getIsSelectable) =>
                                ListPopup<String>(
                                    list: list,
                                    itemBuilder: itemBuilder,
                                    onItemTapped: onItemTapped,
                                    width: 300,
                                    getIsSelectable: getIsSelectable),
                        progressPosition: horizontal
                            ? ProgressPosition.child
                            : ProgressPosition.popup,
                        progressDecoratorBuilder:
                            (context, waiting, mirrored, child) =>
                                ProgressDecorator(
                          waiting: waiting,
                          mirrored: mirrored,
                          child: child,
                          progressBackgroundColor:
                              horizontal ? Colors.transparent : null,
                          progressValueColor: horizontal
                              ? AlwaysStoppedAnimation(
                                  Colors.blueAccent.withOpacity(0.2))
                              : null,
                          progressHeight: horizontal ? null : 2.0,
                        ),
                        position: _position,
                        offset:
                            Offset(_offsetX.toDouble(), _offsetY.toDouble()),
                        requiredSpace: _requiredSpace.toDouble(),
                        screenPadding: EdgeInsets.symmetric(
                            horizontal: _screenPaddingHorizontal.toDouble(),
                            vertical: _screenPaddingVertical.toDouble()),
                        animation: _animation,
                        animationDuration:
                            Duration(milliseconds: _animationDurationMs),
                      );
                    }),
                  ),
                ),

                const SizedBox(height: 16),

                // MenuItemCombo
                buildComboContainer(
                    title: 'MenuItemCombo',
                    child: SizedBox(
                      width: _comboWidth.toDouble(),
                      child: Row(
                        children: [
                          MenuItemCombo<String>(
                            item: MenuItem(
                                'File',
                                () => [
                                      MenuItem('New'),
                                      MenuItem.separator,
                                      MenuItem('Open'),
                                      MenuItem('Save'),
                                      MenuItem('Save As...'),
                                      MenuItem.separator,
                                      MenuItem(
                                          'Recent',
                                          () => [
                                                MenuItem('Folders', () async {
                                                  await Future.delayed(Duration(
                                                      milliseconds: 500));
                                                  return [
                                                    MenuItem('Folder 1'),
                                                    MenuItem('Folder 2'),
                                                    MenuItem('Folder 3'),
                                                  ];
                                                }),
                                                MenuItem('Files', () async {
                                                  await Future.delayed(Duration(
                                                      milliseconds: 500));
                                                  return [
                                                    MenuItem('File 1'),
                                                    MenuItem('File 2'),
                                                    MenuItem('File 3'),
                                                  ];
                                                }),
                                              ]),
                                      MenuItem.separator,
                                      MenuItem('Exit'),
                                    ]),
                            itemBuilder: (context, item) => Padding(
                              padding: const EdgeInsets.all(16),
                              child: Text(item.item),
                            ),
                            onItemTapped: (value) {
                              final dialog = AlertDialog(
                                  content: Text('${value.item} tapped!'));
                              showDialog(
                                  context: context, builder: (_) => dialog);
                            },
                            autoOpen: _autoOpen,
                          ),
                          MenuItemCombo<String>(
                            item: MenuItem(
                                'Edit',
                                () => [
                                      MenuItem('Undo'),
                                      MenuItem('Redo'),
                                      MenuItem.separator,
                                      MenuItem('Cut'),
                                      MenuItem('Copy'),
                                      MenuItem('Paste'),
                                      MenuItem.separator,
                                      MenuItem('Find'),
                                    ]),
                            itemBuilder: (context, item) => Padding(
                              padding: const EdgeInsets.all(16),
                              child: item.item == 'Edit'
                                  ? Text(item.item)
                                  : ConstrainedBox(
                                      constraints: BoxConstraints(minWidth: 80),
                                      child: Text(item.item)),
                            ),
                            onItemTapped: (value) {
                              final dialog = AlertDialog(
                                  content: Text('${value.item} tapped!'));
                              showDialog(
                                  context: context, builder: (_) => dialog);
                            },
                            autoOpen: _autoOpen,
                          ),
                        ],
                      ),
                    )),
              ]),
            ),

            const SizedBox(height: 800),
          ])
        ],
      ),
    );
  }
}

class TestPopup extends StatefulWidget {
  const TestPopup({
    Key key,
    @required this.mirrored,
    @required this.width,
    @required this.itemsCount,
    @required this.onClose,
    @required this.animated,
    this.radius = Radius.zero,
  }) : super(key: key);

  final bool mirrored;
  final double width;
  final int itemsCount;
  final VoidCallback onClose;
  final bool animated;
  final Radius radius;

  @override
  _TestPopupState createState() => _TestPopupState(width, itemsCount);
}

class _TestPopupState extends State<TestPopup>
    with SingleTickerProviderStateMixin {
  _TestPopupState(this._width, this._itemsCount);

  double _width;
  final int _itemsCount;
  AnimationController _controller;

  void animatedClose() => _controller.animateBack(0.0);

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(milliseconds: _customAnimationDurationMs),
      vsync: this,
      value: widget.animated ? 0.0 : 1.0,
    );
    _controller.animateTo(1.0);
  }

  @override
  Widget build(BuildContext context) {
    final close = Ink(
      decoration: BoxDecoration(
        color: Colors.blueAccent,
        borderRadius: BorderRadius.only(
          topLeft: !widget.mirrored ? widget.radius : Radius.zero,
          topRight: !widget.mirrored ? widget.radius : Radius.zero,
          bottomLeft: widget.mirrored ? widget.radius : Radius.zero,
          bottomRight: widget.mirrored ? widget.radius : Radius.zero,
        ),
      ),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          IconButton(
            icon: Icon(Icons.close, color: Colors.white),
            onPressed: widget.onClose,
          ),
        ],
      ),
    );
    final size = Ink(
      decoration: BoxDecoration(color: Colors.blueAccent.withOpacity(0.7)),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          IconButton(
            icon: Icon(Icons.remove, color: Colors.white),
            onPressed: () =>
                setState(() => _width = math.max(_width - 24, 108)),
          ),
          IconButton(
            icon: Icon(Icons.add, color: Colors.white),
            onPressed: () => setState(() => _width += 24),
          ),
        ],
      ),
    );
    final content = Column(
        children: Iterable.generate(_itemsCount)
            .map((_) => ListTile(title: Text('Item ${_ + 1}'), onTap: () {}))
            .toList());

    return Material(
      elevation: 4,
      borderRadius: BorderRadius.all(widget.radius),
      child: AnimatedContainer(
        width: _width,
        duration: const Duration(milliseconds: _customAnimationDurationMs),
        decoration:
            BoxDecoration(borderRadius: BorderRadius.all(widget.radius)),
        child: ScaleTransition(
          scale: _controller,
          child: Column(children: [
            if (widget.mirrored) ...[
              content,
              size,
              close
            ] else ...[
              close,
              size,
              content
            ],
          ]),
        ),
      ),
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

class IntTextInputFormatter extends TextInputFormatter {
  IntTextInputFormatter({this.minValue, this.maxValue});

  final int minValue;
  final int maxValue;

  String format(String oldValue, String newValue) {
    if (newValue?.isNotEmpty != true) return '';
    if (newValue.contains('-')) return oldValue;

    var i = int.tryParse(newValue);
    if (i == null) return oldValue;
    if (minValue != null && i < minValue) i = minValue;
    if (maxValue != null && i > maxValue) i = maxValue;

    return i.toString();
  }

  @override
  TextEditingValue formatEditUpdate(
      TextEditingValue oldValue, TextEditingValue newValue) {
    final value = format(oldValue.text, newValue.text);
    return value != newValue.text
        ? newValue.copyWith(
            text: value,
            selection: TextSelection.collapsed(offset: value.length))
        : newValue.copyWith(text: value);
  }
}

class TextHelper {
  static String _camelToWords(String value) {
    final codes = value.runes
        .skip(1)
        .map((_) => String.fromCharCode(_))
        .map((_) => _.toUpperCase() == _ ? ' $_' : _)
        .expand((_) => _.runes);

    return value[0].toUpperCase() + String.fromCharCodes(codes);
  }

  static String enumToString(dynamic value) =>
      value == null ? '' : _camelToWords(value.toString().split('.')[1]);
}