metro_ui 0.2.0 copy "metro_ui: ^0.2.0" to clipboard
metro_ui: ^0.2.0 copied to clipboard

A Flutter component library inspired by Microsoft's Windows 8 Modern UI design language.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:metro_ui/metro_ui.dart';

import 'gallery/catalog.dart';
import 'gallery/gallery_demo_support.dart';
import 'gallery/gallery_home.dart';
import 'gallery/gallery_navigation.dart';

void main() => runApp(const MetroGalleryApp());

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

  @override
  State<MetroGalleryApp> createState() => _MetroGalleryAppState();
}

class _MetroGalleryAppState extends State<MetroGalleryApp> {
  GalleryDestinationId _selectedDestination = GalleryDestinationId.home;
  GalleryComponent? _selectedComponent;
  bool _dark = false;
  Locale _locale = const Locale('en');
  bool _notifications = true;
  bool _syncEnabled = true;
  bool _favorite = false;
  DateTime _eventDate = DateTime(2026, 8, 3);
  MetroTime _eventTime = const MetroTime(hour: 14, minute: 30);
  String? _travelCity = 'seattle';
  int _flipViewIndex = 0;
  bool _semanticZoomedOut = false;
  int _semanticZoomGroupIndex = 0;
  int _copies = 3;
  double _temperature = 21.5;
  double _volume = 65;
  MetroRangeValues _comfortRange = const MetroRangeValues(18, 26);
  MetroDataGridSort? _albumSort;
  late final MetroSelectionController<int> _viewModeController =
      MetroSelectionController<int>(selectedValues: const [0]);
  late final MetroSelectionController<String> _librarySelectionController =
      MetroSelectionController<String>(
        mode: MetroSelectionMode.multiple,
        selectedValues: const ['documents'],
      );
  late final MetroSelectionController<GalleryAlbum> _albumSelectionController =
      MetroSelectionController<GalleryAlbum>(mode: MetroSelectionMode.multiple);
  late final PageController _semanticZoomPageController = PageController();
  Color _accent = MetroColors.cobalt;
  String _lastAction = 'Choose a control';
  final _tilesKey = GlobalKey();
  final _buttonsKey = GlobalKey();
  final _progressKey = GlobalKey();
  final _overlaysKey = GlobalKey();
  final _selectionKey = GlobalKey();
  final _textInputsKey = GlobalKey();
  final _choiceInputsKey = GlobalKey();
  final _pickersKey = GlobalKey();
  final _slidersKey = GlobalKey();
  final _flipViewKey = GlobalKey();
  final _semanticZoomKey = GlobalKey();
  final _pivotKey = GlobalKey();
  final _dataGridKey = GlobalKey();

  GalleryDestination get _destination =>
      galleryDestinationOf(_selectedDestination);

  bool _shows(GalleryDestinationId destination) {
    return _selectedDestination == destination ||
        _selectedDestination == GalleryDestinationId.allControls;
  }

  void _selectDestination(GalleryDestinationId destination) {
    _updateState(() {
      _selectedDestination = destination;
      _selectedComponent = null;
    }, 'Opened ${galleryDestinationOf(destination).title}');
  }

  void _selectComponent(GalleryComponent component) {
    _updateState(() {
      _selectedDestination = component.destination;
      _selectedComponent = component;
    }, 'Found ${component.name}');
  }

  void _revealSelectedComponent() {
    final component = _selectedComponent;
    if (component == null) return;
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (!mounted || _selectedComponent != component) return;
      final targetContext = _componentTarget(component.name)?.currentContext;
      if (targetContext == null) return;
      Scrollable.ensureVisible(targetContext, alignment: 0.08);
    });
  }

  GlobalKey? _componentTarget(String name) {
    return switch (name) {
      'MetroTile' ||
      'MetroLiveTile' ||
      'MetroTileGrid' ||
      'MetroPage' ||
      'MetroEntrance' => _tilesKey,
      'MetroButton' ||
      'MetroIconButton' ||
      'MetroBackButton' ||
      'MetroCommandBar' ||
      'MetroCommandButton' => _buttonsKey,
      'MetroProgressRing' || 'MetroProgressBar' => _progressKey,
      'MetroDialog' || 'MetroFlyout' || 'MetroTooltip' => _overlaysKey,
      'MetroCheckBox' ||
      'MetroRadioButton' ||
      'MetroToggleSwitch' ||
      'MetroSelectionGroup' ||
      'MetroListTile' ||
      'MetroSelectableListTile' => _selectionKey,
      'MetroSearchBox' ||
      'MetroTextField' ||
      'MetroTextFormField' => _textInputsKey,
      'MetroNumberBox' || 'MetroComboBox' => _choiceInputsKey,
      'MetroDatePicker' || 'MetroTimePicker' => _pickersKey,
      'MetroSlider' || 'MetroRangeSlider' => _slidersKey,
      'MetroFlipView' => _flipViewKey,
      'MetroSemanticZoom' => _semanticZoomKey,
      'MetroPivot' || 'MetroPageRoute' => _pivotKey,
      'MetroDataGrid' => _dataGridKey,
      _ => null,
    };
  }

  void _updateState(VoidCallback update, String action) {
    setState(() {
      update();
      _lastAction = action;
    });
  }

  @override
  Widget build(BuildContext context) {
    final theme = _dark
        ? MetroThemeData.dark(accentColor: _accent)
        : MetroThemeData.light(accentColor: _accent);
    final highContrastTheme = _dark
        ? MetroThemeData.highContrastDark()
        : MetroThemeData.highContrastLight();

    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Metro UI Gallery',
      locale: _locale,
      localizationsDelegates: const [
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
        MetroLocalizations.delegate,
      ],
      supportedLocales: MetroLocalizations.supportedLocales,
      theme: ThemeData(
        brightness: _dark ? Brightness.dark : Brightness.light,
        materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
      ),
      home: AnimatedMetroTheme(
        data: theme,
        highContrastData: highContrastTheme,
        child: MetroCommandBarLayer(
          commandBar: _buildCommandBar(),
          child: LayoutBuilder(
            builder: (context, constraints) {
              final showNavigation = constraints.maxWidth >= 1080;
              return Row(
                children: [
                  if (showNavigation)
                    GalleryNavigation(
                      selected: _selectedDestination,
                      onSelected: _selectDestination,
                      onComponentSelected: _selectComponent,
                    ),
                  Expanded(
                    child: MetroPage(
                      title: Text(
                        _selectedComponent?.name ?? _destination.title,
                      ),
                      actions: [
                        MetroTooltip(
                          message: _locale.languageCode == 'zh'
                              ? 'Use English Metro defaults'
                              : '使用中文 Metro 默认文本',
                          child: MetroIconButton(
                            icon: const Icon(Icons.language),
                            onPressed: () {
                              setState(() {
                                _locale = _locale.languageCode == 'zh'
                                    ? const Locale('en')
                                    : const Locale('zh');
                              });
                            },
                            semanticLabel: _locale.languageCode == 'zh'
                                ? 'Use English locale'
                                : 'Use Chinese locale',
                          ),
                        ),
                        MetroTooltip(
                          message: _dark ? 'Use light theme' : 'Use dark theme',
                          child: MetroIconButton(
                            icon: Icon(
                              _dark ? Icons.light_mode : Icons.dark_mode,
                            ),
                            onPressed: () => setState(() => _dark = !_dark),
                            semanticLabel: _dark
                                ? 'Use light theme'
                                : 'Use dark theme',
                          ),
                        ),
                      ],
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          if (!showNavigation)
                            CompactGalleryNavigation(
                              selected: _selectedDestination,
                              onSelected: _selectDestination,
                              onComponentSelected: _selectComponent,
                            ),
                          if (_selectedDestination == GalleryDestinationId.home)
                            MetroEntrance(
                              key: const ValueKey<String>('gallery-home'),
                              child: GalleryHome(
                                onSelected: _selectDestination,
                                accentPicker: GalleryAccentPicker(
                                  selected: _accent,
                                  onSelected: (color) =>
                                      setState(() => _accent = color),
                                ),
                              ),
                            ),
                          if (_selectedDestination != GalleryDestinationId.home)
                            MetroEntrance(
                              key: ValueKey<String>(
                                'gallery-intro-${_selectedDestination.name}-'
                                '${_selectedComponent?.name}',
                              ),
                              child: GalleryPageIntroduction(
                                destination: _destination,
                                selectedComponent: _selectedComponent,
                                onRevealSelectedComponent:
                                    _selectedComponent == null
                                    ? null
                                    : _revealSelectedComponent,
                              ),
                            ),
                          if (_selectedDestination !=
                              GalleryDestinationId.home) ...[
                            if (_shows(GalleryDestinationId.tiles)) ...[
                              const GallerySectionHeading(
                                title: 'Modern UI for Flutter',
                                description:
                                    'Typography, flat color, direct interaction, and '
                                    'directional motion—without acrylic, elevation, or '
                                    'decorative chrome.',
                              ),
                              const SizedBox(height: MetroSpacing.lg),
                              GalleryAccentPicker(
                                selected: _accent,
                                onSelected: (color) =>
                                    setState(() => _accent = color),
                              ),
                              const SizedBox(height: MetroSpacing.xl),
                              GallerySectionHeading(
                                key: _tilesKey,
                                title: 'Tiles',
                              ),
                              const SizedBox(height: MetroSpacing.sm),
                              MetroFocusTraversalGroup.spatial(
                                debugLabel: 'Gallery tiles',
                                child: MetroTileGrid(
                                  children: [
                                    MetroTile(
                                      icon: const Icon(Icons.mail_outline),
                                      title: 'Mail',
                                      subtitle: '3 unread',
                                      badge: const Text('3'),
                                      onPressed: () => _record('Mail tile'),
                                    ),
                                    MetroLiveTile(
                                      size: MetroTileSize.wide,
                                      title: 'Photos',
                                      subtitle: 'Live tile',
                                      badge: const Text('12'),
                                      backgroundColor: MetroColors.magenta,
                                      transition:
                                          MetroLiveTileTransition.slideLeft,
                                      onPressed: () => _record('Photos tile'),
                                      frames: const [
                                        MetroLiveTileFrame(
                                          id: 'collection',
                                          semanticLabel:
                                              'Photos, 12 new memories',
                                          child: Center(
                                            child: Icon(
                                              Icons.photo_outlined,
                                              size: 48,
                                            ),
                                          ),
                                        ),
                                        MetroLiveTileFrame(
                                          id: 'highlights',
                                          semanticLabel:
                                              'Photos, highlights from this week',
                                          child: Align(
                                            alignment: Alignment.centerLeft,
                                            child: Text('12 NEW\nMEMORIES'),
                                          ),
                                        ),
                                      ],
                                    ),
                                    MetroTile(
                                      icon: const Icon(Icons.cloud_outlined),
                                      title: 'Weather',
                                      backgroundColor: MetroColors.teal,
                                      badge: const Text('24°'),
                                      badgePosition:
                                          MetroTileBadgePosition.topEnd,
                                      onPressed: () => _record('Weather tile'),
                                    ),
                                    MetroTile(
                                      icon: const Icon(Icons.settings_outlined),
                                      title: 'Disabled',
                                      onPressed: null,
                                    ),
                                  ],
                                ),
                              ),
                              const SizedBox(height: MetroSpacing.xl),
                            ],
                            if (_shows(
                              GalleryDestinationId.buttonsAndFeedback,
                            )) ...[
                              GallerySectionHeading(
                                key: _buttonsKey,
                                title: 'Buttons and feedback',
                              ),
                              const SizedBox(height: MetroSpacing.sm),
                              Wrap(
                                crossAxisAlignment: WrapCrossAlignment.center,
                                spacing: MetroSpacing.sm,
                                runSpacing: MetroSpacing.sm,
                                children: [
                                  MetroButton(
                                    onPressed: () => _record('Standard button'),
                                    child: const Text('STANDARD'),
                                  ),
                                  MetroButton.accent(
                                    onPressed: () => _record('Accent button'),
                                    child: const Text('ACCENT'),
                                  ),
                                  MetroButtonTheme(
                                    data: const MetroButtonThemeData(
                                      style: MetroButtonStyle(
                                        borderColor: WidgetStatePropertyAll(
                                          MetroColors.orange,
                                        ),
                                        borderWidth: WidgetStatePropertyAll(3),
                                      ),
                                    ),
                                    child: MetroButton(
                                      onPressed: () =>
                                          _record('Locally themed button'),
                                      child: const Text('LOCAL THEME'),
                                    ),
                                  ),
                                  const MetroButton(
                                    onPressed: null,
                                    child: Text('DISABLED'),
                                  ),
                                  MetroBackButton(
                                    onPressed: () => _record('Back button'),
                                    semanticLabel: 'Back button example',
                                  ),
                                  const MetroProgressRing(
                                    semanticLabel: 'Loading',
                                  ),
                                  const MetroProgressRing(
                                    value: 0.68,
                                    semanticLabel: '68 percent complete',
                                  ),
                                ],
                              ),
                              const SizedBox(height: MetroSpacing.lg),
                            ],
                            if (_shows(
                              GalleryDestinationId.selectionAndLists,
                            )) ...[
                              GallerySectionHeading(
                                key: _selectionKey,
                                title: 'Selection and lists',
                              ),
                              const SizedBox(height: MetroSpacing.sm),
                              Wrap(
                                spacing: MetroSpacing.lg,
                                runSpacing: MetroSpacing.sm,
                                children: [
                                  MetroCheckBox(
                                    value: _syncEnabled,
                                    onChanged: (value) {
                                      setState(
                                        () => _syncEnabled = value ?? false,
                                      );
                                    },
                                    label: const Text('Sync settings'),
                                  ),
                                  MetroSelectionGroup<int>(
                                    controller: _viewModeController,
                                    onChanged: (values) {
                                      _record('View mode ${values.single}');
                                    },
                                    child: const Wrap(
                                      spacing: MetroSpacing.lg,
                                      runSpacing: MetroSpacing.sm,
                                      children: [
                                        MetroRadioButton<int>(
                                          value: 0,
                                          label: Text('Compact'),
                                        ),
                                        MetroRadioButton<int>(
                                          value: 1,
                                          label: Text('Comfortable'),
                                        ),
                                      ],
                                    ),
                                  ),
                                ],
                              ),
                              const SizedBox(height: MetroSpacing.md),
                              ConstrainedBox(
                                constraints: const BoxConstraints(
                                  maxWidth: 520,
                                ),
                                child: MetroSelectionGroup<String>(
                                  controller: _librarySelectionController,
                                  onChanged: (values) {
                                    _record(
                                      '${values.length} library items selected',
                                    );
                                  },
                                  child: Column(
                                    children: [
                                      MetroSelectableListTile<String>(
                                        value: 'documents',
                                        leading: const Icon(
                                          Icons.folder_outlined,
                                        ),
                                        title: const Text('Documents'),
                                        subtitle: const Text(
                                          'Multi-select item',
                                        ),
                                        trailing: const Icon(
                                          Icons.chevron_right,
                                        ),
                                      ),
                                      MetroSelectableListTile<String>(
                                        value: 'pictures',
                                        leading: const Icon(
                                          Icons.image_outlined,
                                        ),
                                        title: const Text('Pictures'),
                                        subtitle: const Text(
                                          'Multi-select item',
                                        ),
                                        trailing: const Icon(
                                          Icons.chevron_right,
                                        ),
                                      ),
                                    ],
                                  ),
                                ),
                              ),
                              const SizedBox(height: MetroSpacing.md),
                              MetroToggleSwitch(
                                value: _notifications,
                                onChanged: (value) {
                                  setState(() => _notifications = value);
                                },
                                label: Text(
                                  _notifications
                                      ? 'Notifications on'
                                      : 'Notifications off',
                                ),
                                semanticLabel: 'Notifications',
                              ),
                              const SizedBox(height: MetroSpacing.lg),
                            ],
                            if (_shows(
                              GalleryDestinationId.buttonsAndFeedback,
                            )) ...[
                              GallerySectionHeading(
                                key: _progressKey,
                                title: 'Progress',
                                description:
                                    'Determinate and indeterminate feedback for ongoing work.',
                              ),
                              const SizedBox(height: MetroSpacing.sm),
                              const SizedBox(
                                width: 420,
                                child: Column(
                                  children: [
                                    MetroProgressBar(value: 0.64),
                                    SizedBox(height: MetroSpacing.md),
                                    MetroProgressBar(
                                      semanticLabel: 'Loading content',
                                    ),
                                  ],
                                ),
                              ),
                              const SizedBox(height: MetroSpacing.xl),
                              GallerySectionHeading(
                                key: _overlaysKey,
                                title: 'Dialogs, flyouts, and tooltips',
                                description:
                                    'Transient surfaces for decisions, settings, and contextual help.',
                              ),
                              const SizedBox(height: MetroSpacing.sm),
                              Wrap(
                                spacing: MetroSpacing.sm,
                                runSpacing: MetroSpacing.sm,
                                children: [
                                  Builder(
                                    builder: (context) => MetroButton(
                                      onPressed: () =>
                                          _showAboutDialog(context),
                                      child: const Text('OPEN DIALOG'),
                                    ),
                                  ),
                                  Builder(
                                    builder: (context) => MetroButton(
                                      onPressed: () =>
                                          _showSettingsFlyout(context),
                                      child: const Text('OPEN FLYOUT'),
                                    ),
                                  ),
                                  MetroTooltip(
                                    message: 'A Metro tooltip example',
                                    child: MetroButton(
                                      onPressed: () =>
                                          _record('Tooltip button'),
                                      child: const Text('HOVER FOR TOOLTIP'),
                                    ),
                                  ),
                                ],
                              ),
                              const SizedBox(height: MetroSpacing.xl),
                            ],
                            if (_shows(
                              GalleryDestinationId.inputsAndPickers,
                            )) ...[
                              GallerySectionHeading(
                                key: _textInputsKey,
                                title: 'Inputs and pickers',
                              ),
                              const SizedBox(height: MetroSpacing.sm),
                              ConstrainedBox(
                                constraints: const BoxConstraints(
                                  maxWidth: 520,
                                ),
                                child: Column(
                                  children: [
                                    MetroSearchBox<String>(
                                      placeholder: 'Search controls',
                                      semanticLabel: 'Search the gallery',
                                      items: const [
                                        MetroSearchBoxItem(
                                          value: 'tiles',
                                          queryText: 'Tiles and live tiles',
                                          child: Text('Tiles and live tiles'),
                                        ),
                                        MetroSearchBoxItem(
                                          value: 'combo-box',
                                          queryText: 'Combo box',
                                          child: Text('Combo box'),
                                        ),
                                        MetroSearchBoxItem(
                                          value: 'number-box',
                                          queryText: 'Number box',
                                          child: Text('Number box'),
                                        ),
                                        MetroSearchBoxItem(
                                          value: 'flip-view',
                                          queryText: 'FlipView',
                                          child: Text('FlipView'),
                                        ),
                                        MetroSearchBoxItem(
                                          value: 'data-grid',
                                          queryText: 'Data grid',
                                          child: Text('Data grid'),
                                        ),
                                      ],
                                      onSelected: (item) {
                                        _record(
                                          'Search suggestion ${item.queryText}',
                                        );
                                      },
                                      onSubmitted: (query) {
                                        _record('Search submitted: $query');
                                      },
                                    ),
                                    const SizedBox(height: MetroSpacing.md),
                                    MetroTextFormField(
                                      autovalidateMode:
                                          AutovalidateMode.onUserInteraction,
                                      label: const Text('Account name'),
                                      placeholder: 'At least four characters',
                                      semanticLabel: 'Account name',
                                      showSuccessWhenValid: true,
                                      successText: const Text(
                                        'Name is available',
                                      ),
                                      supportingText: const Text(
                                        'Validation uses Flutter FormField semantics',
                                      ),
                                      validator: (value) =>
                                          (value?.length ?? 0) < 4
                                          ? 'Use at least four characters'
                                          : null,
                                    ),
                                    const SizedBox(height: MetroSpacing.md),
                                    MetroTextFormField.password(
                                      autovalidateMode:
                                          AutovalidateMode.onUserInteraction,
                                      label: const Text('Password'),
                                      placeholder: 'Enter a password',
                                      semanticLabel: 'Password',
                                      supportingText: const Text(
                                        'Eight characters minimum',
                                      ),
                                      validator: (value) =>
                                          (value?.length ?? 0) < 8
                                          ? 'Password is too short'
                                          : null,
                                    ),
                                    const SizedBox(height: MetroSpacing.md),
                                    GallerySectionHeading(
                                      key: _choiceInputsKey,
                                      title: 'Numbers and choices',
                                    ),
                                    const SizedBox(height: MetroSpacing.sm),
                                    Wrap(
                                      spacing: MetroSpacing.sm,
                                      runSpacing: MetroSpacing.sm,
                                      children: [
                                        SizedBox(
                                          width: 250,
                                          child: MetroNumberBox<int>(
                                            value: _copies,
                                            min: 1,
                                            max: 99,
                                            largeChange: 10,
                                            prefix: const Text('COPIES'),
                                            semanticLabel: 'Copies',
                                            onChanged: (value) {
                                              if (value == null) return;
                                              _updateState(
                                                () => _copies = value,
                                                'Copies $value',
                                              );
                                            },
                                          ),
                                        ),
                                        MetroNumberBoxTheme(
                                          data: const MetroNumberBoxThemeData(
                                            style: MetroNumberBoxStyle(
                                              buttonBackgroundColor:
                                                  WidgetStatePropertyAll(
                                                    MetroColors.orange,
                                                  ),
                                            ),
                                          ),
                                          child: SizedBox(
                                            width: 250,
                                            child: MetroNumberBox<double>(
                                              value: _temperature,
                                              min: -20,
                                              max: 50,
                                              smallChange: 0.5,
                                              largeChange: 5,
                                              decimalPlaces: 1,
                                              formatter: (value) =>
                                                  value == null
                                                  ? ''
                                                  : '${value.toStringAsFixed(1)} °C',
                                              parser: (text) => double.tryParse(
                                                text
                                                    .replaceAll('°C', '')
                                                    .trim(),
                                              ),
                                              semanticLabel: 'Temperature',
                                              onChanged: (value) {
                                                if (value == null) return;
                                                _updateState(
                                                  () => _temperature = value,
                                                  'Temperature $value',
                                                );
                                              },
                                            ),
                                          ),
                                        ),
                                        const SizedBox(
                                          width: 220,
                                          child: MetroNumberBox<int>(
                                            value: 10,
                                            onChanged: null,
                                            placeholder: 'Disabled',
                                          ),
                                        ),
                                      ],
                                    ),
                                    const SizedBox(height: MetroSpacing.md),
                                    Wrap(
                                      spacing: MetroSpacing.sm,
                                      runSpacing: MetroSpacing.sm,
                                      children: [
                                        SizedBox(
                                          width: 250,
                                          child: MetroComboBox<String>(
                                            value: _travelCity,
                                            placeholder: const Text(
                                              'Choose a destination',
                                            ),
                                            semanticLabel: 'Travel destination',
                                            items: const [
                                              MetroComboBoxItem(
                                                value: 'london',
                                                child: Text('London'),
                                              ),
                                              MetroComboBoxItem(
                                                value: 'seattle',
                                                child: Text('Seattle'),
                                              ),
                                              MetroComboBoxItem(
                                                value: 'closed',
                                                enabled: false,
                                                semanticLabel:
                                                    'Unavailable destination',
                                                child: Text('Unavailable'),
                                              ),
                                              MetroComboBoxItem(
                                                value: 'tokyo',
                                                child: Text('Tokyo'),
                                              ),
                                            ],
                                            onChanged: (value) {
                                              _updateState(
                                                () => _travelCity = value,
                                                'Destination $value',
                                              );
                                            },
                                          ),
                                        ),
                                        MetroComboBoxTheme(
                                          data: const MetroComboBoxThemeData(
                                            style: MetroComboBoxStyle(
                                              borderColor:
                                                  WidgetStatePropertyAll(
                                                    MetroColors.orange,
                                                  ),
                                              borderWidth:
                                                  WidgetStatePropertyAll(2),
                                            ),
                                          ),
                                          child: SizedBox(
                                            width: 220,
                                            child: MetroComboBox<String>(
                                              placeholder: const Text(
                                                'Local theme',
                                              ),
                                              items: const [
                                                MetroComboBoxItem(
                                                  value: 'compact',
                                                  child: Text('Compact'),
                                                ),
                                                MetroComboBoxItem(
                                                  value: 'comfortable',
                                                  child: Text('Comfortable'),
                                                ),
                                              ],
                                              onChanged: (value) {
                                                _record('Combo density $value');
                                              },
                                            ),
                                          ),
                                        ),
                                        const SizedBox(
                                          width: 190,
                                          child: MetroComboBox<String>(
                                            placeholder: Text('Disabled'),
                                            disabledPlaceholder: Text(
                                              'Unavailable',
                                            ),
                                            items: [
                                              MetroComboBoxItem(
                                                value: 'disabled',
                                                child: Text('Disabled'),
                                              ),
                                            ],
                                          ),
                                        ),
                                      ],
                                    ),
                                    const SizedBox(height: MetroSpacing.md),
                                    GallerySectionHeading(
                                      key: _pickersKey,
                                      title: 'Date and time',
                                    ),
                                    const SizedBox(height: MetroSpacing.sm),
                                    Wrap(
                                      spacing: MetroSpacing.sm,
                                      runSpacing: MetroSpacing.sm,
                                      children: [
                                        SizedBox(
                                          width: 280,
                                          child: MetroDatePicker(
                                            selected: _eventDate,
                                            firstDate: DateTime(2024),
                                            lastDate: DateTime(2030, 12, 31),
                                            monthFormatter: (context, month) =>
                                                _monthNames[month - 1],
                                            onChanged: (date) {
                                              _updateState(
                                                () => _eventDate = date,
                                                'Event date selected',
                                              );
                                            },
                                            semanticLabel: 'Event date',
                                          ),
                                        ),
                                        SizedBox(
                                          width: 210,
                                          child: MetroTimePicker(
                                            selected: _eventTime,
                                            minuteIncrement: 15,
                                            onChanged: (time) {
                                              _updateState(
                                                () => _eventTime = time,
                                                'Event time selected',
                                              );
                                            },
                                            semanticLabel: 'Event time',
                                          ),
                                        ),
                                      ],
                                    ),
                                    const SizedBox(height: MetroSpacing.lg),
                                    GallerySectionHeading(
                                      key: _slidersKey,
                                      title: 'Sliders',
                                    ),
                                    const SizedBox(height: MetroSpacing.sm),
                                    Align(
                                      alignment: Alignment.centerLeft,
                                      child: Text('Volume ${_volume.round()}'),
                                    ),
                                    MetroSlider(
                                      value: _volume,
                                      min: 0,
                                      max: 100,
                                      divisions: 20,
                                      tickPlacement:
                                          MetroSliderTickPlacement.after,
                                      semanticLabel: 'Volume',
                                      semanticFormatterCallback: (value) =>
                                          '${value.round()} percent',
                                      onChanged: (value) {
                                        _updateState(
                                          () => _volume = value,
                                          'Volume ${value.round()}',
                                        );
                                      },
                                    ),
                                    const SizedBox(height: MetroSpacing.md),
                                    Align(
                                      alignment: Alignment.centerLeft,
                                      child: Text(
                                        'Comfort range ${_comfortRange.start.round()}–'
                                        '${_comfortRange.end.round()} °C',
                                      ),
                                    ),
                                    MetroRangeSlider(
                                      values: _comfortRange,
                                      min: 10,
                                      max: 35,
                                      divisions: 25,
                                      minimumRange: 2,
                                      tickPlacement:
                                          MetroSliderTickPlacement.after,
                                      startSemanticLabel:
                                          'Minimum comfort temperature',
                                      endSemanticLabel:
                                          'Maximum comfort temperature',
                                      semanticFormatterCallback: (value) =>
                                          '${value.round()} degrees',
                                      onChanged: (values) {
                                        _updateState(
                                          () => _comfortRange = values,
                                          'Comfort range ${values.start.round()}–'
                                          '${values.end.round()}',
                                        );
                                      },
                                    ),
                                  ],
                                ),
                              ),
                              const SizedBox(height: MetroSpacing.md),
                            ],
                            if (_shows(GalleryDestinationId.navigation)) ...[
                              GallerySectionHeading(
                                key: _flipViewKey,
                                title: 'FlipView',
                                description:
                                    'Direct swipe navigation with keyboard commands, '
                                    'hover controls, banners, and optional circular paging.',
                              ),
                              const SizedBox(height: MetroSpacing.sm),
                              ConstrainedBox(
                                constraints: const BoxConstraints(
                                  maxWidth: 720,
                                ),
                                child: SizedBox(
                                  height: 300,
                                  child: MetroFlipView(
                                    index: _flipViewIndex,
                                    circular: true,
                                    showIndicators: true,
                                    semanticLabel: 'Modern UI feature stories',
                                    onChanged: (index) {
                                      _updateState(
                                        () => _flipViewIndex = index,
                                        'FlipView page ${index + 1}',
                                      );
                                    },
                                    items: const [
                                      MetroFlipViewItem(
                                        semanticLabel:
                                            'Direct interaction story',
                                        banner: Text(
                                          'SWIPE, CLICK, OR USE THE ARROW KEYS',
                                        ),
                                        child: GalleryFlipViewStory(
                                          color: MetroColors.cobalt,
                                          eyebrow: 'MODERN UI',
                                          title: 'CONTENT\nBEFORE CHROME',
                                          icon: Icons.swipe,
                                        ),
                                      ),
                                      MetroFlipViewItem(
                                        semanticLabel: 'Typography story',
                                        banner: Text(
                                          'BOLD SCALE CREATES THE HIERARCHY',
                                        ),
                                        child: GalleryFlipViewStory(
                                          color: MetroColors.magenta,
                                          eyebrow: 'TYPOGRAPHY',
                                          title: 'CLEAR, FAST,\nCONFIDENT',
                                          icon: Icons.text_fields,
                                        ),
                                      ),
                                      MetroFlipViewItem(
                                        semanticLabel: 'Motion story',
                                        banner: Text(
                                          'DIRECTIONAL MOTION PRESERVES CONTEXT',
                                        ),
                                        child: GalleryFlipViewStory(
                                          color: MetroColors.emerald,
                                          eyebrow: 'MOTION',
                                          title: 'ONE SURFACE,\nNEXT STORY',
                                          icon: Icons.arrow_forward,
                                        ),
                                      ),
                                    ],
                                  ),
                                ),
                              ),
                              const SizedBox(height: MetroSpacing.md),
                              GallerySectionHeading(
                                key: _semanticZoomKey,
                                title: 'Semantic zoom',
                                description:
                                    'Pinch, press Ctrl+Minus, or use the transient minus '
                                    'button to move between detailed groups and a compact '
                                    'overview. Choosing a group restores its detailed page.',
                              ),
                              const SizedBox(height: MetroSpacing.sm),
                              ConstrainedBox(
                                constraints: const BoxConstraints(
                                  maxWidth: 720,
                                ),
                                child: MetroSemanticZoom(
                                  height: 320,
                                  semanticLabel: 'Gallery component groups',
                                  zoomedOut: _semanticZoomedOut,
                                  onZoomedOutChanged: (zoomedOut) {
                                    _updateState(
                                      () => _semanticZoomedOut = zoomedOut,
                                      zoomedOut
                                          ? 'Semantic zoom overview'
                                          : 'Semantic zoom details',
                                    );
                                  },
                                  zoomedInView: PageView.builder(
                                    controller: _semanticZoomPageController,
                                    itemCount: _semanticZoomGroups.length,
                                    onPageChanged: (index) {
                                      setState(
                                        () => _semanticZoomGroupIndex = index,
                                      );
                                    },
                                    itemBuilder: (context, index) {
                                      return GallerySemanticZoomGroupPage(
                                        group: _semanticZoomGroups[index],
                                        onItemPressed: (item) {
                                          _record('Semantic zoom item $item');
                                        },
                                      );
                                    },
                                  ),
                                  zoomedOutView: GallerySemanticZoomOverview(
                                    groups: _semanticZoomGroups,
                                    selectedIndex: _semanticZoomGroupIndex,
                                    onSelected: _openSemanticZoomGroup,
                                  ),
                                ),
                              ),
                              const SizedBox(height: MetroSpacing.md),
                            ],
                            if (_shows(GalleryDestinationId.data)) ...[
                              GallerySectionHeading(
                                key: _dataGridKey,
                                title: 'Data grid',
                                description:
                                    'Controlled sorting, shared row selection, horizontal '
                                    'columns, and keyboard navigation.',
                              ),
                              const SizedBox(height: MetroSpacing.sm),
                              ConstrainedBox(
                                constraints: const BoxConstraints(
                                  maxWidth: 720,
                                ),
                                child: MetroDataGrid<GalleryAlbum>(
                                  autofocus: true,
                                  columns: _albumColumns,
                                  height: 264,
                                  onSelectionChanged: (album, selected) {
                                    _record(
                                      '${selected ? 'Selected' : 'Cleared'} ${album.title}',
                                    );
                                  },
                                  onSortChanged: (sort) {
                                    _updateState(
                                      () => _albumSort = sort,
                                      'Sorted albums ${sort.direction.name}',
                                    );
                                  },
                                  rowSemanticLabelBuilder: (album, index) =>
                                      '${album.title}, ${album.artist}, ${album.year}',
                                  rows: _sortedAlbums,
                                  selectionController:
                                      _albumSelectionController,
                                  sort: _albumSort,
                                ),
                              ),
                              const SizedBox(height: MetroSpacing.md),
                            ],
                            if (_shows(GalleryDestinationId.navigation)) ...[
                              GallerySectionHeading(
                                key: _pivotKey,
                                title: 'Pivot and page routes',
                                description:
                                    'Section switching and directional transitions for Metro pages.',
                              ),
                              const SizedBox(height: MetroSpacing.sm),
                              SizedBox(
                                height: 190,
                                child: MetroPivot(
                                  items: const [
                                    MetroPivotItem(
                                      header: Text('RECENT'),
                                      child: Align(
                                        alignment: Alignment.topLeft,
                                        child: Text('Recently used components'),
                                      ),
                                    ),
                                    MetroPivotItem(
                                      header: Text('FAVORITES'),
                                      child: Align(
                                        alignment: Alignment.topLeft,
                                        child: Text(
                                          'Pinned component examples',
                                        ),
                                      ),
                                    ),
                                  ],
                                ),
                              ),
                              const SizedBox(height: MetroSpacing.md),
                              Builder(
                                builder: (context) {
                                  return MetroButton(
                                    onPressed: () => _showMotionPage(context),
                                    child: const Text('OPEN TRANSITION PAGE'),
                                  );
                                },
                              ),
                              const SizedBox(height: MetroSpacing.lg),
                            ],
                            Text('Last action: $_lastAction'),
                            const SizedBox(height: MetroSpacing.xl),
                          ],
                        ],
                      ),
                    ),
                  ),
                ],
              );
            },
          ),
        ),
      ),
    );
  }

  Widget _buildCommandBar() {
    return MetroCommandBar(
      leading: const Text('COMMANDS'),
      commands: [
        MetroCommandButton(
          icon: const Icon(Icons.refresh),
          label: const Text('Refresh'),
          onPressed: () => _record('Refresh command'),
          semanticLabel: 'Refresh gallery',
        ),
        MetroCommandButton(
          icon: Icon(_favorite ? Icons.favorite : Icons.favorite_border),
          label: const Text('Favorite'),
          onPressed: () =>
              _updateState(() => _favorite = !_favorite, 'Favorite command'),
          selected: _favorite,
          semanticLabel: 'Favorite gallery',
        ),
        const MetroCommandButton(
          icon: Icon(Icons.share_outlined),
          label: Text('Share'),
          onPressed: null,
          semanticLabel: 'Share unavailable',
        ),
        Builder(
          builder: (context) {
            return MetroCommandButton(
              icon: const Icon(Icons.tune),
              label: const Text('Settings'),
              onPressed: () => _showSettingsFlyout(context),
              semanticLabel: 'Open settings flyout',
            );
          },
        ),
        Builder(
          builder: (context) {
            return MetroCommandButton(
              icon: const Icon(Icons.info_outline),
              label: const Text('About'),
              onPressed: () => _showAboutDialog(context),
              semanticLabel: 'About Metro UI',
            );
          },
        ),
      ],
    );
  }

  void _record(String action) => setState(() => _lastAction = action);

  static const _monthNames = <String>[
    'JAN',
    'FEB',
    'MAR',
    'APR',
    'MAY',
    'JUN',
    'JUL',
    'AUG',
    'SEP',
    'OCT',
    'NOV',
    'DEC',
  ];

  static const _albums = <GalleryAlbum>[
    GalleryAlbum('Discovery', 'Daft Punk', 2001),
    GalleryAlbum('Homework', 'Daft Punk', 1997),
    GalleryAlbum('In Colour', 'Jamie xx', 2015),
    GalleryAlbum('Settle', 'Disclosure', 2013),
    GalleryAlbum('Immunity', 'Jon Hopkins', 2013),
  ];

  static const _semanticZoomGroups = <GallerySemanticZoomGroup>[
    GallerySemanticZoomGroup(
      label: 'LAYOUT',
      description: 'Content-first surfaces and responsive Metro composition.',
      color: MetroColors.cobalt,
      items: ['Tile', 'LiveTile', 'Semantic zoom'],
    ),
    GallerySemanticZoomGroup(
      label: 'NAVIGATION',
      description: 'Directional movement that keeps collection context.',
      color: MetroColors.magenta,
      items: ['Pivot', 'FlipView', 'PageRoute'],
    ),
    GallerySemanticZoomGroup(
      label: 'INPUT',
      description: 'Direct, square controls with explicit interaction states.',
      color: MetroColors.teal,
      items: ['SearchBox', 'NumberBox', 'Slider'],
    ),
  ];

  static final _albumColumns = <MetroDataGridColumn<GalleryAlbum>>[
    MetroDataGridColumn<GalleryAlbum>(
      key: 'title',
      label: const Text('TITLE'),
      semanticLabel: 'Sort albums by title',
      sortable: true,
      minimumWidth: 190,
      cellBuilder: (context, album, index) => Text(album.title),
    ),
    MetroDataGridColumn<GalleryAlbum>(
      key: 'artist',
      label: const Text('ARTIST'),
      semanticLabel: 'Sort albums by artist',
      sortable: true,
      minimumWidth: 170,
      cellBuilder: (context, album, index) => Text(album.artist),
    ),
    MetroDataGridColumn<GalleryAlbum>(
      key: 'year',
      label: const Text('YEAR'),
      semanticLabel: 'Sort albums by year',
      sortable: true,
      width: 90,
      alignment: AlignmentDirectional.centerEnd,
      headerAlignment: AlignmentDirectional.centerEnd,
      cellBuilder: (context, album, index) => Text('${album.year}'),
    ),
  ];

  List<GalleryAlbum> get _sortedAlbums {
    final albums = List<GalleryAlbum>.of(_albums);
    final sort = _albumSort;
    if (sort == null) return albums;
    albums.sort((first, second) {
      final comparison = switch (sort.columnKey) {
        'artist' => first.artist.compareTo(second.artist),
        'year' => first.year.compareTo(second.year),
        _ => first.title.compareTo(second.title),
      };
      return sort.direction == MetroDataGridSortDirection.ascending
          ? comparison
          : -comparison;
    });
    return albums;
  }

  @override
  void dispose() {
    _semanticZoomPageController.dispose();
    _viewModeController.dispose();
    _librarySelectionController.dispose();
    _albumSelectionController.dispose();
    super.dispose();
  }

  void _openSemanticZoomGroup(int index) {
    if (_semanticZoomPageController.hasClients) {
      _semanticZoomPageController.jumpToPage(index);
    }
    _updateState(() {
      _semanticZoomGroupIndex = index;
      _semanticZoomedOut = false;
    }, 'Semantic zoom ${_semanticZoomGroups[index].label}');
  }

  void _showMotionPage(BuildContext context) {
    Navigator.of(context).push(
      MetroPageRoute<void>(
        context: context,
        builder: (pageContext) {
          return MetroPage(
            title: const Text('Directional motion'),
            actions: [
              MetroIconButton(
                icon: const Icon(Icons.close),
                onPressed: () => Navigator.of(pageContext).pop(),
                semanticLabel: 'Close transition page',
              ),
            ],
            child: const Text(
              'MetroPageRoute captures the active Metro theme and follows '
              'the logical reading direction.',
            ),
          );
        },
      ),
    );
  }

  void _showAboutDialog(BuildContext context) {
    showMetroDialog<void>(
      context: context,
      barrierDismissible: true,
      barrierLabel: 'Dismiss about dialog',
      builder: (dialogContext) {
        return MetroDialog(
          semanticLabel: 'About Metro UI dialog',
          title: const Text('Metro UI for Flutter'),
          content: const Text(
            'A Flutter-native interpretation of the Windows 8 Modern UI '
            'design language.',
          ),
          actions: [
            MetroButton.accent(
              onPressed: () => Navigator.of(dialogContext).pop(),
              child: const Text('CLOSE'),
            ),
          ],
        );
      },
    );
  }

  void _showSettingsFlyout(BuildContext context) {
    showMetroFlyout<void>(
      context: context,
      barrierDismissible: true,
      barrierLabel: 'Dismiss settings flyout',
      builder: (flyoutContext) {
        return StatefulBuilder(
          builder: (context, setFlyoutState) {
            return MetroFlyout(
              semanticLabel: 'Gallery settings flyout',
              title: const Text('Settings'),
              actions: [
                MetroIconButton(
                  icon: const Icon(Icons.close),
                  onPressed: () => Navigator.of(flyoutContext).pop(),
                  semanticLabel: 'Close settings',
                  variant: MetroButtonVariant.accent,
                ),
              ],
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    'Experience',
                    style: MetroTheme.of(context).typography.title,
                  ),
                  const SizedBox(height: MetroSpacing.md),
                  MetroToggleSwitch(
                    value: _notifications,
                    onChanged: (value) {
                      setState(() => _notifications = value);
                      setFlyoutState(() {});
                    },
                    label: const Text('Notifications'),
                  ),
                  const SizedBox(height: MetroSpacing.md),
                  MetroCheckBox(
                    value: _syncEnabled,
                    onChanged: (value) {
                      setState(() => _syncEnabled = value ?? false);
                      setFlyoutState(() {});
                    },
                    label: const Text('Sync settings'),
                  ),
                ],
              ),
            );
          },
        );
      },
    );
  }
}
1
likes
160
points
58
downloads
screenshot

Documentation

API reference

Publisher

verified publishergt610.dpdns.org

Weekly Downloads

A Flutter component library inspired by Microsoft's Windows 8 Modern UI design language.

Repository (GitHub)
View/report issues
Contributing

Topics

#metro #modern-ui #windows #desktop #widget

License

MIT (license)

Dependencies

flutter

More

Packages that depend on metro_ui