From 0fa15e04acd8db185ab17e5d1d351ecbcbd7b51f Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Tue, 11 Apr 2023 15:59:37 -0400 Subject: [PATCH] [web] Add benchmarks for the Material3 Components screen (#124354) [web] Add benchmarks for the Material3 Components screen --- .../lib/src/web/bench_material_3.dart | 2344 +++++++++++++++++ .../macrobenchmarks/lib/web_benchmarks.dart | 2 + 2 files changed, 2346 insertions(+) create mode 100644 dev/benchmarks/macrobenchmarks/lib/src/web/bench_material_3.dart diff --git a/dev/benchmarks/macrobenchmarks/lib/src/web/bench_material_3.dart b/dev/benchmarks/macrobenchmarks/lib/src/web/bench_material_3.dart new file mode 100644 index 00000000000..a531281c6d4 --- /dev/null +++ b/dev/benchmarks/macrobenchmarks/lib/src/web/bench_material_3.dart @@ -0,0 +1,2344 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'recorder.dart'; + +/// Measures how expensive it is to construct the material 3 components screen. +class BenchMaterial3Components extends WidgetBuildRecorder { + BenchMaterial3Components() : super(name: benchmarkName); + + static const String benchmarkName = 'bench_material3_components'; + + @override + Widget createWidget() { + return const Material3Components(); + } +} + +const SizedBox rowDivider = SizedBox(width: 20); +const SizedBox colDivider = SizedBox(height: 10); +const double smallSpacing = 10.0; +const double cardWidth = 115; +const double widthConstraint = 450; + +class Material3Components extends StatefulWidget { + const Material3Components({super.key}); + + @override + State createState() => _Material3ComponentsState(); +} + +class _Material3ComponentsState extends State { + final GlobalKey scaffoldKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + key: scaffoldKey, + body: Row( + children: [ + Expanded( + child: FirstComponentList( + showNavBottomBar: true, + scaffoldKey: scaffoldKey, + showSecondList: true, + ), + ), + Expanded( + child: SecondComponentList(scaffoldKey: scaffoldKey), + ), + ], + ), + ), + ); + } +} + +class FirstComponentList extends StatelessWidget { + const FirstComponentList({ + super.key, + required this.showNavBottomBar, + required this.scaffoldKey, + required this.showSecondList, + }); + + final bool showNavBottomBar; + final GlobalKey scaffoldKey; + final bool showSecondList; + + @override + Widget build(BuildContext context) { + // Fully traverse this list before moving on. + return FocusTraversalGroup( + child: ListView( + padding: showSecondList + ? const EdgeInsetsDirectional.only(end: smallSpacing) + : EdgeInsets.zero, + children: [ + const Actions(), + colDivider, + const Communication(), + colDivider, + const Containment(), + if (!showSecondList) ...[ + colDivider, + Navigation(scaffoldKey: scaffoldKey), + colDivider, + const Selection(), + colDivider, + const TextInputs() + ], + ], + ), + ); + } +} + +class SecondComponentList extends StatelessWidget { + const SecondComponentList({ + super.key, + required this.scaffoldKey, + }); + + final GlobalKey scaffoldKey; + + @override + Widget build(BuildContext context) { + // Fully traverse this list before moving on. + return FocusTraversalGroup( + child: ListView( + padding: const EdgeInsetsDirectional.only(end: smallSpacing), + children: [ + Navigation(scaffoldKey: scaffoldKey), + colDivider, + const Selection(), + colDivider, + const TextInputs(), + ], + ), + ); + } +} + +class Actions extends StatelessWidget { + const Actions({super.key}); + + @override + Widget build(BuildContext context) { + return const ComponentGroupDecoration(label: 'Actions', children: [ + Buttons(), + FloatingActionButtons(), + IconToggleButtons(), + SegmentedButtons(), + ]); + } +} + +class Communication extends StatelessWidget { + const Communication({super.key}); + + @override + Widget build(BuildContext context) { + return const ComponentGroupDecoration(label: 'Communication', children: [ + NavigationBars( + selectedIndex: 1, + isExampleBar: true, + isBadgeExample: true, + ), + ProgressIndicators(), + SnackBarSection(), + ]); + } +} + +class Containment extends StatelessWidget { + const Containment({super.key}); + + @override + Widget build(BuildContext context) { + return const ComponentGroupDecoration(label: 'Containment', children: [ + BottomSheetSection(), + Cards(), + Dialogs(), + Dividers(), + ]); + } +} + +class Navigation extends StatelessWidget { + const Navigation({super.key, required this.scaffoldKey}); + + final GlobalKey scaffoldKey; + + @override + Widget build(BuildContext context) { + return ComponentGroupDecoration(label: 'Navigation', children: [ + const BottomAppBars(), + const NavigationBars( + selectedIndex: 0, + isExampleBar: true, + ), + NavigationDrawers(scaffoldKey: scaffoldKey), + const NavigationRails(), + const Tabs(), + const TopAppBars(), + ]); + } +} + +class Selection extends StatelessWidget { + const Selection({super.key}); + + @override + Widget build(BuildContext context) { + return const ComponentGroupDecoration(label: 'Selection', children: [ + Checkboxes(), + Chips(), + Menus(), + Radios(), + Sliders(), + Switches(), + ]); + } +} + +class TextInputs extends StatelessWidget { + const TextInputs({super.key}); + + @override + Widget build(BuildContext context) { + return const ComponentGroupDecoration( + label: 'Text inputs', + children: [TextFields()], + ); + } +} + +class Buttons extends StatefulWidget { + const Buttons({super.key}); + + @override + State createState() => _ButtonsState(); +} + +class _ButtonsState extends State { + @override + Widget build(BuildContext context) { + return const ComponentDecoration( + label: 'Common buttons', + tooltipMessage: + 'Use ElevatedButton, FilledButton, FilledButton.tonal, OutlinedButton, or TextButton', + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + ButtonsWithoutIcon(isDisabled: false), + ButtonsWithIcon(), + ButtonsWithoutIcon(isDisabled: true), + ], + ), + ), + ); + } +} + +class ButtonsWithoutIcon extends StatelessWidget { + const ButtonsWithoutIcon({super.key, required this.isDisabled}); + + final bool isDisabled; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 5.0), + child: IntrinsicWidth( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ElevatedButton( + onPressed: isDisabled ? null : () {}, + child: const Text('Elevated'), + ), + colDivider, + FilledButton( + onPressed: isDisabled ? null : () {}, + child: const Text('Filled'), + ), + colDivider, + FilledButton.tonal( + onPressed: isDisabled ? null : () {}, + child: const Text('Filled tonal'), + ), + colDivider, + OutlinedButton( + onPressed: isDisabled ? null : () {}, + child: const Text('Outlined'), + ), + colDivider, + TextButton( + onPressed: isDisabled ? null : () {}, + child: const Text('Text'), + ), + ], + ), + ), + ); + } +} + +class ButtonsWithIcon extends StatelessWidget { + const ButtonsWithIcon({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: IntrinsicWidth( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ElevatedButton.icon( + onPressed: () {}, + icon: const Icon(Icons.add), + label: const Text('Icon'), + ), + colDivider, + FilledButton.icon( + onPressed: () {}, + label: const Text('Icon'), + icon: const Icon(Icons.add), + ), + colDivider, + FilledButton.tonalIcon( + onPressed: () {}, + label: const Text('Icon'), + icon: const Icon(Icons.add), + ), + colDivider, + OutlinedButton.icon( + onPressed: () {}, + icon: const Icon(Icons.add), + label: const Text('Icon'), + ), + colDivider, + TextButton.icon( + onPressed: () {}, + icon: const Icon(Icons.add), + label: const Text('Icon'), + ) + ], + ), + ), + ); + } +} + +class FloatingActionButtons extends StatelessWidget { + const FloatingActionButtons({super.key}); + + @override + Widget build(BuildContext context) { + return ComponentDecoration( + label: 'Floating action buttons', + tooltipMessage: + 'Use FloatingActionButton or FloatingActionButton.extended', + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + runSpacing: smallSpacing, + spacing: smallSpacing, + children: [ + FloatingActionButton.small( + onPressed: () {}, + tooltip: 'Small', + child: const Icon(Icons.add), + ), + FloatingActionButton.extended( + onPressed: () {}, + tooltip: 'Extended', + icon: const Icon(Icons.add), + label: const Text('Create'), + ), + FloatingActionButton( + onPressed: () {}, + tooltip: 'Standard', + child: const Icon(Icons.add), + ), + FloatingActionButton.large( + onPressed: () {}, + tooltip: 'Large', + child: const Icon(Icons.add), + ), + ], + ), + ); + } +} + +class Cards extends StatelessWidget { + const Cards({super.key}); + + @override + Widget build(BuildContext context) { + return ComponentDecoration( + label: 'Cards', + tooltipMessage: 'Use Card', + child: Wrap( + alignment: WrapAlignment.spaceEvenly, + children: [ + SizedBox( + width: cardWidth, + child: Card( + child: Container( + padding: const EdgeInsets.fromLTRB(10, 5, 5, 10), + child: Column( + children: [ + Align( + alignment: Alignment.topRight, + child: IconButton( + icon: const Icon(Icons.more_vert), + onPressed: () {}, + ), + ), + const SizedBox(height: 20), + const Align( + alignment: Alignment.bottomLeft, + child: Text('Elevated'), + ) + ], + ), + ), + ), + ), + SizedBox( + width: cardWidth, + child: Card( + color: Theme.of(context).colorScheme.surfaceVariant, + elevation: 0, + child: Container( + padding: const EdgeInsets.fromLTRB(10, 5, 5, 10), + child: Column( + children: [ + Align( + alignment: Alignment.topRight, + child: IconButton( + icon: const Icon(Icons.more_vert), + onPressed: () {}, + ), + ), + const SizedBox(height: 20), + const Align( + alignment: Alignment.bottomLeft, + child: Text('Filled'), + ) + ], + ), + ), + ), + ), + SizedBox( + width: cardWidth, + child: Card( + elevation: 0, + shape: RoundedRectangleBorder( + side: BorderSide( + color: Theme.of(context).colorScheme.outline, + ), + borderRadius: const BorderRadius.all(Radius.circular(12)), + ), + child: Container( + padding: const EdgeInsets.fromLTRB(10, 5, 5, 10), + child: Column( + children: [ + Align( + alignment: Alignment.topRight, + child: IconButton( + icon: const Icon(Icons.more_vert), + onPressed: () {}, + ), + ), + const SizedBox(height: 20), + const Align( + alignment: Alignment.bottomLeft, + child: Text('Outlined'), + ) + ], + ), + ), + ), + ), + ], + ), + ); + } +} + +class _ClearButton extends StatelessWidget { + const _ClearButton({required this.controller}); + + final TextEditingController controller; + + @override + Widget build(BuildContext context) => IconButton( + icon: const Icon(Icons.clear), + onPressed: () => controller.clear(), + ); +} + +class TextFields extends StatefulWidget { + const TextFields({super.key}); + + @override + State createState() => _TextFieldsState(); +} + +class _TextFieldsState extends State { + final TextEditingController _controllerFilled = TextEditingController(); + final TextEditingController _controllerOutlined = TextEditingController(); + + @override + Widget build(BuildContext context) { + return ComponentDecoration( + label: 'Text fields', + tooltipMessage: 'Use TextField with different InputDecoration', + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(smallSpacing), + child: TextField( + controller: _controllerFilled, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.search), + suffixIcon: _ClearButton(controller: _controllerFilled), + labelText: 'Filled', + hintText: 'hint text', + helperText: 'supporting text', + filled: true, + ), + ), + ), + Padding( + padding: const EdgeInsets.all(smallSpacing), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: SizedBox( + width: 200, + child: TextField( + maxLength: 10, + maxLengthEnforcement: MaxLengthEnforcement.none, + controller: _controllerFilled, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.search), + suffixIcon: _ClearButton(controller: _controllerFilled), + labelText: 'Filled', + hintText: 'hint text', + helperText: 'supporting text', + filled: true, + errorText: 'error text', + ), + ), + ), + ), + const SizedBox(width: smallSpacing), + Flexible( + child: SizedBox( + width: 200, + child: TextField( + controller: _controllerFilled, + enabled: false, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.search), + suffixIcon: _ClearButton(controller: _controllerFilled), + labelText: 'Disabled', + hintText: 'hint text', + helperText: 'supporting text', + filled: true, + ), + ), + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.all(smallSpacing), + child: TextField( + controller: _controllerOutlined, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.search), + suffixIcon: _ClearButton(controller: _controllerOutlined), + labelText: 'Outlined', + hintText: 'hint text', + helperText: 'supporting text', + border: const OutlineInputBorder(), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(smallSpacing), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: SizedBox( + width: 200, + child: TextField( + controller: _controllerOutlined, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.search), + suffixIcon: + _ClearButton(controller: _controllerOutlined), + labelText: 'Outlined', + hintText: 'hint text', + helperText: 'supporting text', + errorText: 'error text', + border: const OutlineInputBorder(), + filled: true, + ), + ), + ), + ), + const SizedBox(width: smallSpacing), + Flexible( + child: SizedBox( + width: 200, + child: TextField( + controller: _controllerOutlined, + enabled: false, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.search), + suffixIcon: + _ClearButton(controller: _controllerOutlined), + labelText: 'Disabled', + hintText: 'hint text', + helperText: 'supporting text', + border: const OutlineInputBorder(), + filled: true, + ), + ), + ), + ), + ])), + ], + ), + ); + } +} + +class Dialogs extends StatefulWidget { + const Dialogs({super.key}); + + @override + State createState() => _DialogsState(); +} + +class _DialogsState extends State { + void openDialog(BuildContext context) { + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: const Text('What is a dialog?'), + content: const Text( + 'A dialog is a type of modal window that appears in front of app content to provide critical information, or prompt for a decision to be made.'), + actions: [ + TextButton( + child: const Text('Okay'), + onPressed: () => Navigator.of(context).pop(), + ), + FilledButton( + child: const Text('Dismiss'), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ), + ); + } + + void openFullscreenDialog(BuildContext context) { + showDialog( + context: context, + builder: (BuildContext context) => Dialog.fullscreen( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Scaffold( + appBar: AppBar( + title: const Text('Full-screen dialog'), + centerTitle: false, + leading: IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.of(context).pop(), + ), + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ), + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return ComponentDecoration( + label: 'Dialog', + tooltipMessage: + 'Use showDialog with Dialog.fullscreen, AlertDialog, or SimpleDialog', + child: Wrap( + alignment: WrapAlignment.spaceBetween, + children: [ + TextButton( + child: const Text( + 'Show dialog', + style: TextStyle(fontWeight: FontWeight.bold), + ), + onPressed: () => openDialog(context), + ), + TextButton( + child: const Text( + 'Show full-screen dialog', + style: TextStyle(fontWeight: FontWeight.bold), + ), + onPressed: () => openFullscreenDialog(context), + ), + ], + ), + ); + } +} + +class Dividers extends StatelessWidget { + const Dividers({super.key}); + + @override + Widget build(BuildContext context) { + return const ComponentDecoration( + label: 'Dividers', + tooltipMessage: 'Use Divider or VerticalDivider', + child: Column( + children: [ + Divider(key: Key('divider')), + ], + ), + ); + } +} + +class Switches extends StatelessWidget { + const Switches({super.key}); + + @override + Widget build(BuildContext context) { + return const ComponentDecoration( + label: 'Switches', + tooltipMessage: 'Use SwitchListTile or Switch', + child: Column( + children: [ + SwitchRow(isEnabled: true), + SwitchRow(isEnabled: false), + ], + ), + ); + } +} + +class SwitchRow extends StatefulWidget { + const SwitchRow({super.key, required this.isEnabled}); + + final bool isEnabled; + + @override + State createState() => _SwitchRowState(); +} + +class _SwitchRowState extends State { + bool value0 = false; + bool value1 = true; + + final MaterialStateProperty thumbIcon = + MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.selected)) { + return const Icon(Icons.check); + } + return const Icon(Icons.close); + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Switch( + value: value0, + onChanged: widget.isEnabled + ? (bool value) { + setState(() { + value0 = value; + }); + } + : null, + ), + Switch( + thumbIcon: thumbIcon, + value: value1, + onChanged: widget.isEnabled + ? (bool value) { + setState(() { + value1 = value; + }); + } + : null, + ), + ], + ); + } +} + +class Checkboxes extends StatefulWidget { + const Checkboxes({super.key}); + + @override + State createState() => _CheckboxesState(); +} + +class _CheckboxesState extends State { + bool? isChecked0 = true; + bool? isChecked1; + bool? isChecked2 = false; + + @override + Widget build(BuildContext context) { + return ComponentDecoration( + label: 'Checkboxes', + tooltipMessage: 'Use CheckboxListTile or Checkbox', + child: Column( + children: [ + CheckboxListTile( + tristate: true, + value: isChecked0, + title: const Text('Option 1'), + onChanged: (bool? value) { + setState(() { + isChecked0 = value; + }); + }, + ), + CheckboxListTile( + tristate: true, + value: isChecked1, + title: const Text('Option 2'), + onChanged: (bool? value) { + setState(() { + isChecked1 = value; + }); + }, + ), + CheckboxListTile( + tristate: true, + value: isChecked2, + title: const Text('Option 3'), + onChanged: (bool? value) { + setState(() { + isChecked2 = value; + }); + }, + ), + const CheckboxListTile( + tristate: true, + title: Text('Option 4'), + value: true, + onChanged: null, + ), + ], + ), + ); + } +} + +enum Value { first, second } + +class Radios extends StatefulWidget { + const Radios({super.key}); + + @override + State createState() => _RadiosState(); +} + +enum Options { option1, option2, option3 } + +class _RadiosState extends State { + Options? _selectedOption = Options.option1; + + @override + Widget build(BuildContext context) { + return ComponentDecoration( + label: 'Radio buttons', + tooltipMessage: 'Use RadioListTile or Radio', + child: Column( + children: [ + RadioListTile( + title: const Text('Option 1'), + value: Options.option1, + groupValue: _selectedOption, + onChanged: (Options? value) { + setState(() { + _selectedOption = value; + }); + }, + ), + RadioListTile( + title: const Text('Option 2'), + value: Options.option2, + groupValue: _selectedOption, + onChanged: (Options? value) { + setState(() { + _selectedOption = value; + }); + }, + ), + RadioListTile( + title: const Text('Option 3'), + value: Options.option3, + groupValue: _selectedOption, + onChanged: null, + ), + ], + ), + ); + } +} + +class ProgressIndicators extends StatefulWidget { + const ProgressIndicators({super.key}); + + @override + State createState() => _ProgressIndicatorsState(); +} + +class _ProgressIndicatorsState extends State { + bool playProgressIndicator = false; + + @override + Widget build(BuildContext context) { + final double? progressValue = playProgressIndicator ? null : 0.7; + + return ComponentDecoration( + label: 'Progress indicators', + tooltipMessage: + 'Use CircularProgressIndicator or LinearProgressIndicator', + child: Column( + children: [ + Row( + children: [ + IconButton( + isSelected: playProgressIndicator, + selectedIcon: const Icon(Icons.pause), + icon: const Icon(Icons.play_arrow), + onPressed: () { + setState(() { + playProgressIndicator = !playProgressIndicator; + }); + }, + ), + Expanded( + child: Row( + children: [ + rowDivider, + CircularProgressIndicator( + value: progressValue, + ), + rowDivider, + Expanded( + child: LinearProgressIndicator( + value: progressValue, + ), + ), + rowDivider, + ], + ), + ), + ], + ), + ], + ), + ); + } +} + +const List appBarDestinations = [ + NavigationDestination( + tooltip: '', + icon: Icon(Icons.widgets_outlined), + label: 'Components', + selectedIcon: Icon(Icons.widgets), + ), + NavigationDestination( + tooltip: '', + icon: Icon(Icons.format_paint_outlined), + label: 'Color', + selectedIcon: Icon(Icons.format_paint), + ), + NavigationDestination( + tooltip: '', + icon: Icon(Icons.text_snippet_outlined), + label: 'Typography', + selectedIcon: Icon(Icons.text_snippet), + ), + NavigationDestination( + tooltip: '', + icon: Icon(Icons.invert_colors_on_outlined), + label: 'Elevation', + selectedIcon: Icon(Icons.opacity), + ) +]; + +const List exampleBarDestinations = [ + NavigationDestination( + tooltip: '', + icon: Icon(Icons.explore_outlined), + label: 'Explore', + selectedIcon: Icon(Icons.explore), + ), + NavigationDestination( + tooltip: '', + icon: Icon(Icons.pets_outlined), + label: 'Pets', + selectedIcon: Icon(Icons.pets), + ), + NavigationDestination( + tooltip: '', + icon: Icon(Icons.account_box_outlined), + label: 'Account', + selectedIcon: Icon(Icons.account_box), + ) +]; + +List barWithBadgeDestinations = [ + NavigationDestination( + tooltip: '', + icon: Badge.count(count: 1000, child: const Icon(Icons.mail_outlined)), + label: 'Mail', + selectedIcon: Badge.count(count: 1000, child: const Icon(Icons.mail)), + ), + const NavigationDestination( + tooltip: '', + icon: Badge(label: Text('10'), child: Icon(Icons.chat_bubble_outline)), + label: 'Chat', + selectedIcon: Badge(label: Text('10'), child: Icon(Icons.chat_bubble)), + ), + const NavigationDestination( + tooltip: '', + icon: Badge(child: Icon(Icons.group_outlined)), + label: 'Rooms', + selectedIcon: Badge(child: Icon(Icons.group_rounded)), + ), + NavigationDestination( + tooltip: '', + icon: Badge.count(count: 3, child: const Icon(Icons.videocam_outlined)), + label: 'Meet', + selectedIcon: Badge.count(count: 3, child: const Icon(Icons.videocam)), + ) +]; + +class NavigationBars extends StatefulWidget { + const NavigationBars({ + super.key, + this.onSelectItem, + required this.selectedIndex, + required this.isExampleBar, + this.isBadgeExample = false, + }); + + final void Function(int)? onSelectItem; + final int selectedIndex; + final bool isExampleBar; + final bool isBadgeExample; + + @override + State createState() => _NavigationBarsState(); +} + +class _NavigationBarsState extends State { + late int selectedIndex; + + @override + void initState() { + super.initState(); + selectedIndex = widget.selectedIndex; + } + + @override + void didUpdateWidget(covariant NavigationBars oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.selectedIndex != oldWidget.selectedIndex) { + selectedIndex = widget.selectedIndex; + } + } + + @override + Widget build(BuildContext context) { + // App NavigationBar should get first focus. + Widget navigationBar = Focus( + autofocus: !(widget.isExampleBar || widget.isBadgeExample), + child: NavigationBar( + selectedIndex: selectedIndex, + onDestinationSelected: (int index) { + setState(() { + selectedIndex = index; + }); + if (!widget.isExampleBar) { + widget.onSelectItem!(index); + } + }, + destinations: widget.isExampleBar && widget.isBadgeExample + ? barWithBadgeDestinations + : widget.isExampleBar + ? exampleBarDestinations + : appBarDestinations, + ), + ); + + if (widget.isExampleBar && widget.isBadgeExample) { + navigationBar = ComponentDecoration( + label: 'Badges', + tooltipMessage: 'Use Badge or Badge.count', + child: navigationBar); + } else if (widget.isExampleBar) { + navigationBar = ComponentDecoration( + label: 'Navigation bar', + tooltipMessage: 'Use NavigationBar', + child: navigationBar); + } + + return navigationBar; + } +} + +class IconToggleButtons extends StatefulWidget { + const IconToggleButtons({super.key}); + + @override + State createState() => _IconToggleButtonsState(); +} + +class _IconToggleButtonsState extends State { + @override + Widget build(BuildContext context) { + return const ComponentDecoration( + label: 'Icon buttons', + tooltipMessage: 'Use IconButton', + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Column( + // Standard IconButton + children: [ + IconToggleButton( + isEnabled: true, + tooltip: 'Standard', + ), + colDivider, + IconToggleButton( + isEnabled: false, + tooltip: 'Standard (disabled)', + ), + ], + ), + Column( + children: [ + // Filled IconButton + IconToggleButton( + isEnabled: true, + tooltip: 'Filled', + getDefaultStyle: enabledFilledButtonStyle, + ), + colDivider, + IconToggleButton( + isEnabled: false, + tooltip: 'Filled (disabled)', + getDefaultStyle: disabledFilledButtonStyle, + ), + ], + ), + Column( + children: [ + // Filled Tonal IconButton + IconToggleButton( + isEnabled: true, + tooltip: 'Filled tonal', + getDefaultStyle: enabledFilledTonalButtonStyle, + ), + colDivider, + IconToggleButton( + isEnabled: false, + tooltip: 'Filled tonal (disabled)', + getDefaultStyle: disabledFilledTonalButtonStyle, + ), + ], + ), + Column( + children: [ + // Outlined IconButton + IconToggleButton( + isEnabled: true, + tooltip: 'Outlined', + getDefaultStyle: enabledOutlinedButtonStyle, + ), + colDivider, + IconToggleButton( + isEnabled: false, + tooltip: 'Outlined (disabled)', + getDefaultStyle: disabledOutlinedButtonStyle, + ), + ], + ), + ], + ), + ); + } +} + +class IconToggleButton extends StatefulWidget { + const IconToggleButton({ + required this.isEnabled, + required this.tooltip, + this.getDefaultStyle, + super.key, + }); + + final bool isEnabled; + final String tooltip; + final ButtonStyle? Function(bool, ColorScheme)? getDefaultStyle; + + @override + State createState() => _IconToggleButtonState(); +} + +class _IconToggleButtonState extends State { + bool selected = false; + + @override + Widget build(BuildContext context) { + final ColorScheme colors = Theme.of(context).colorScheme; + final VoidCallback? onPressed = widget.isEnabled + ? () { + setState(() { + selected = !selected; + }); + } + : null; + final ButtonStyle? style = widget.getDefaultStyle?.call(selected, colors); + + return IconButton( + visualDensity: VisualDensity.standard, + isSelected: selected, + tooltip: widget.tooltip, + icon: const Icon(Icons.settings_outlined), + selectedIcon: const Icon(Icons.settings), + onPressed: onPressed, + style: style, + ); + } +} + +ButtonStyle enabledFilledButtonStyle(bool selected, ColorScheme colors) { + return IconButton.styleFrom( + foregroundColor: selected ? colors.onPrimary : colors.primary, + backgroundColor: selected ? colors.primary : colors.surfaceVariant, + disabledForegroundColor: colors.onSurface.withOpacity(0.38), + disabledBackgroundColor: colors.onSurface.withOpacity(0.12), + hoverColor: selected + ? colors.onPrimary.withOpacity(0.08) + : colors.primary.withOpacity(0.08), + focusColor: selected + ? colors.onPrimary.withOpacity(0.12) + : colors.primary.withOpacity(0.12), + highlightColor: selected + ? colors.onPrimary.withOpacity(0.12) + : colors.primary.withOpacity(0.12), + ); +} + +ButtonStyle disabledFilledButtonStyle(bool selected, ColorScheme colors) { + return IconButton.styleFrom( + disabledForegroundColor: colors.onSurface.withOpacity(0.38), + disabledBackgroundColor: colors.onSurface.withOpacity(0.12), + ); +} + +ButtonStyle enabledFilledTonalButtonStyle(bool selected, ColorScheme colors) { + return IconButton.styleFrom( + foregroundColor: + selected ? colors.onSecondaryContainer : colors.onSurfaceVariant, + backgroundColor: + selected ? colors.secondaryContainer : colors.surfaceVariant, + hoverColor: selected + ? colors.onSecondaryContainer.withOpacity(0.08) + : colors.onSurfaceVariant.withOpacity(0.08), + focusColor: selected + ? colors.onSecondaryContainer.withOpacity(0.12) + : colors.onSurfaceVariant.withOpacity(0.12), + highlightColor: selected + ? colors.onSecondaryContainer.withOpacity(0.12) + : colors.onSurfaceVariant.withOpacity(0.12), + ); +} + +ButtonStyle disabledFilledTonalButtonStyle(bool selected, ColorScheme colors) { + return IconButton.styleFrom( + disabledForegroundColor: colors.onSurface.withOpacity(0.38), + disabledBackgroundColor: colors.onSurface.withOpacity(0.12), + ); +} + +ButtonStyle enabledOutlinedButtonStyle(bool selected, ColorScheme colors) { + return IconButton.styleFrom( + backgroundColor: selected ? colors.inverseSurface : null, + hoverColor: selected + ? colors.onInverseSurface.withOpacity(0.08) + : colors.onSurfaceVariant.withOpacity(0.08), + focusColor: selected + ? colors.onInverseSurface.withOpacity(0.12) + : colors.onSurfaceVariant.withOpacity(0.12), + highlightColor: selected + ? colors.onInverseSurface.withOpacity(0.12) + : colors.onSurface.withOpacity(0.12), + side: BorderSide(color: colors.outline), + ).copyWith( + foregroundColor: MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.selected)) { + return colors.onInverseSurface; + } + if (states.contains(MaterialState.pressed)) { + return colors.onSurface; + } + return null; + }), + ); +} + +ButtonStyle disabledOutlinedButtonStyle(bool selected, ColorScheme colors) { + return IconButton.styleFrom( + disabledForegroundColor: colors.onSurface.withOpacity(0.38), + disabledBackgroundColor: + selected ? colors.onSurface.withOpacity(0.12) : null, + side: selected ? null : BorderSide(color: colors.outline.withOpacity(0.12)), + ); +} + +class Chips extends StatefulWidget { + const Chips({super.key}); + + @override + State createState() => _ChipsState(); +} + +class _ChipsState extends State { + bool isFiltered = true; + + @override + Widget build(BuildContext context) { + return ComponentDecoration( + label: 'Chips', + tooltipMessage: + 'Use ActionChip, FilterChip, or InputChip. \nActionChip can also be used for suggestion chip', + child: Column( + children: [ + Wrap( + spacing: smallSpacing, + runSpacing: smallSpacing, + children: [ + ActionChip( + label: const Text('Assist'), + avatar: const Icon(Icons.event), + onPressed: () {}, + ), + FilterChip( + label: const Text('Filter'), + selected: isFiltered, + onSelected: (bool selected) { + setState(() => isFiltered = selected); + }, + ), + InputChip( + label: const Text('Input'), + onPressed: () {}, + onDeleted: () {}, + ), + ActionChip( + label: const Text('Suggestion'), + onPressed: () {}, + ), + ], + ), + colDivider, + Wrap( + spacing: smallSpacing, + runSpacing: smallSpacing, + children: [ + const ActionChip( + label: Text('Assist'), + avatar: Icon(Icons.event), + ), + FilterChip( + label: const Text('Filter'), + selected: isFiltered, + onSelected: null, + ), + InputChip( + label: const Text('Input'), + onDeleted: () {}, + isEnabled: false, + ), + const ActionChip( + label: Text('Suggestion'), + ), + ], + ), + ], + ), + ); + } +} + +class SegmentedButtons extends StatelessWidget { + const SegmentedButtons({super.key}); + + @override + Widget build(BuildContext context) { + return const ComponentDecoration( + label: 'Segmented buttons', + tooltipMessage: 'Use SegmentedButton', + child: Column( + children: [ + SingleChoice(), + colDivider, + MultipleChoice(), + ], + ), + ); + } +} + +enum Calendar { day, week, month, year } + +class SingleChoice extends StatefulWidget { + const SingleChoice({super.key}); + + @override + State createState() => _SingleChoiceState(); +} + +class _SingleChoiceState extends State { + Calendar calendarView = Calendar.day; + + @override + Widget build(BuildContext context) { + return SegmentedButton( + segments: const >[ + ButtonSegment( + value: Calendar.day, + label: Text('Day'), + icon: Icon(Icons.calendar_view_day)), + ButtonSegment( + value: Calendar.week, + label: Text('Week'), + icon: Icon(Icons.calendar_view_week)), + ButtonSegment( + value: Calendar.month, + label: Text('Month'), + icon: Icon(Icons.calendar_view_month)), + ButtonSegment( + value: Calendar.year, + label: Text('Year'), + icon: Icon(Icons.calendar_today)), + ], + selected: {calendarView}, + onSelectionChanged: (Set newSelection) { + setState(() { + // By default there is only a single segment that can be + // selected at one time, so its value is always the first + // item in the selected set. + calendarView = newSelection.first; + }); + }, + ); + } +} + +enum Sizes { extraSmall, small, medium, large, extraLarge } + +class MultipleChoice extends StatefulWidget { + const MultipleChoice({super.key}); + + @override + State createState() => _MultipleChoiceState(); +} + +class _MultipleChoiceState extends State { + Set selection = {Sizes.large, Sizes.extraLarge}; + + @override + Widget build(BuildContext context) { + return SegmentedButton( + segments: const >[ + ButtonSegment(value: Sizes.extraSmall, label: Text('XS')), + ButtonSegment(value: Sizes.small, label: Text('S')), + ButtonSegment(value: Sizes.medium, label: Text('M')), + ButtonSegment( + value: Sizes.large, + label: Text('L'), + ), + ButtonSegment(value: Sizes.extraLarge, label: Text('XL')), + ], + selected: selection, + onSelectionChanged: (Set newSelection) { + setState(() { + selection = newSelection; + }); + }, + multiSelectionEnabled: true, + ); + } +} + +class SnackBarSection extends StatelessWidget { + const SnackBarSection({super.key}); + + @override + Widget build(BuildContext context) { + return ComponentDecoration( + label: 'Snackbar', + tooltipMessage: + 'Use ScaffoldMessenger.of(context).showSnackBar with SnackBar', + child: TextButton( + onPressed: () { + final SnackBar snackBar = SnackBar( + behavior: SnackBarBehavior.floating, + width: 400.0, + content: const Text('This is a snackbar'), + action: SnackBarAction( + label: 'Close', + onPressed: () {}, + ), + ); + + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + }, + child: const Text( + 'Show snackbar', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ), + ); + } +} + +class BottomSheetSection extends StatefulWidget { + const BottomSheetSection({super.key}); + + @override + State createState() => _BottomSheetSectionState(); +} + +class _BottomSheetSectionState extends State { + bool isNonModalBottomSheetOpen = false; + PersistentBottomSheetController? _nonModalBottomSheetController; + + @override + Widget build(BuildContext context) { + List buttonList = [ + IconButton(onPressed: () {}, icon: const Icon(Icons.share_outlined)), + IconButton(onPressed: () {}, icon: const Icon(Icons.add)), + IconButton(onPressed: () {}, icon: const Icon(Icons.delete_outline)), + IconButton(onPressed: () {}, icon: const Icon(Icons.archive_outlined)), + IconButton(onPressed: () {}, icon: const Icon(Icons.settings_outlined)), + IconButton(onPressed: () {}, icon: const Icon(Icons.favorite_border)), + ]; + const List labelList = [ + Text('Share'), + Text('Add to'), + Text('Trash'), + Text('Archive'), + Text('Settings'), + Text('Favorite') + ]; + + buttonList = List.generate( + buttonList.length, + (int index) => Padding( + padding: const EdgeInsets.fromLTRB(20.0, 30.0, 20.0, 20.0), + child: Column( + children: [ + buttonList[index], + labelList[index], + ], + ), + )); + + return ComponentDecoration( + label: 'Bottom sheet', + tooltipMessage: 'Use showModalBottomSheet or showBottomSheet', + child: Wrap( + alignment: WrapAlignment.spaceEvenly, + children: [ + TextButton( + child: const Text( + 'Show modal bottom sheet', + style: TextStyle(fontWeight: FontWeight.bold), + ), + onPressed: () { + showModalBottomSheet( + context: context, + constraints: const BoxConstraints(maxWidth: 640), + builder: (BuildContext context) { + return SizedBox( + height: 150, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32.0), + child: ListView( + shrinkWrap: true, + scrollDirection: Axis.horizontal, + children: buttonList, + ), + ), + ); + }, + ); + }, + ), + TextButton( + child: Text( + isNonModalBottomSheetOpen + ? 'Hide bottom sheet' + : 'Show bottom sheet', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + onPressed: () { + if (isNonModalBottomSheetOpen) { + _nonModalBottomSheetController?.close(); + setState(() { + isNonModalBottomSheetOpen = false; + }); + return; + } else { + setState(() { + isNonModalBottomSheetOpen = true; + }); + } + + _nonModalBottomSheetController = showBottomSheet( + elevation: 8.0, + context: context, + constraints: const BoxConstraints(maxWidth: 640), + builder: (BuildContext context) { + return SizedBox( + height: 150, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32.0), + child: ListView( + shrinkWrap: true, + scrollDirection: Axis.horizontal, + children: buttonList, + ), + ), + ); + }, + ); + }, + ), + ], + ), + ); + } +} + +class BottomAppBars extends StatelessWidget { + const BottomAppBars({super.key}); + + @override + Widget build(BuildContext context) { + return ComponentDecoration( + label: 'Bottom app bar', + tooltipMessage: 'Use BottomAppBar', + child: Column( + children: [ + SizedBox( + height: 80, + child: Scaffold( + floatingActionButton: FloatingActionButton( + onPressed: () {}, + elevation: 0.0, + child: const Icon(Icons.add), + ), + floatingActionButtonLocation: + FloatingActionButtonLocation.endContained, + bottomNavigationBar: BottomAppBar( + child: Row( + children: [ + const IconButtonAnchorExample(), + IconButton( + tooltip: 'Search', + icon: const Icon(Icons.search), + onPressed: () {}, + ), + IconButton( + tooltip: 'Favorite', + icon: const Icon(Icons.favorite), + onPressed: () {}, + ), + ], + ), + ), + ), + ), + ], + ), + ); + } +} + +class IconButtonAnchorExample extends StatelessWidget { + const IconButtonAnchorExample({super.key}); + + @override + Widget build(BuildContext context) { + return MenuAnchor( + builder: (BuildContext context, MenuController controller, Widget? child) { + return IconButton( + onPressed: () { + if (controller.isOpen) { + controller.close(); + } else { + controller.open(); + } + }, + icon: const Icon(Icons.more_vert), + ); + }, + menuChildren: [ + MenuItemButton( + child: const Text('Menu 1'), + onPressed: () {}, + ), + MenuItemButton( + child: const Text('Menu 2'), + onPressed: () {}, + ), + SubmenuButton( + menuChildren: [ + MenuItemButton( + onPressed: () {}, + child: const Text('Menu 3.1'), + ), + MenuItemButton( + onPressed: () {}, + child: const Text('Menu 3.2'), + ), + MenuItemButton( + onPressed: () {}, + child: const Text('Menu 3.3'), + ), + ], + child: const Text('Menu 3'), + ), + ], + ); + } +} + +class ButtonAnchorExample extends StatelessWidget { + const ButtonAnchorExample({super.key}); + + @override + Widget build(BuildContext context) { + return MenuAnchor( + builder: (BuildContext context, MenuController controller, Widget? child) { + return FilledButton.tonal( + onPressed: () { + if (controller.isOpen) { + controller.close(); + } else { + controller.open(); + } + }, + child: const Text('Show menu'), + ); + }, + menuChildren: [ + MenuItemButton( + leadingIcon: const Icon(Icons.people_alt_outlined), + child: const Text('Item 1'), + onPressed: () {}, + ), + MenuItemButton( + leadingIcon: const Icon(Icons.remove_red_eye_outlined), + child: const Text('Item 2'), + onPressed: () {}, + ), + MenuItemButton( + leadingIcon: const Icon(Icons.refresh), + onPressed: () {}, + child: const Text('Item 3'), + ), + ], + ); + } +} + +class NavigationDrawers extends StatelessWidget { + const NavigationDrawers({super.key, required this.scaffoldKey}); + final GlobalKey scaffoldKey; + + @override + Widget build(BuildContext context) { + return ComponentDecoration( + label: 'Navigation drawer', + tooltipMessage: + 'Use NavigationDrawer. For modal navigation drawers, see Scaffold.endDrawer', + child: Column( + children: [ + const SizedBox(height: 520, child: NavigationDrawerSection()), + colDivider, + colDivider, + TextButton( + child: const Text('Show modal navigation drawer', + style: TextStyle(fontWeight: FontWeight.bold)), + onPressed: () { + scaffoldKey.currentState!.openEndDrawer(); + }, + ), + ], + ), + ); + } +} + +class NavigationDrawerSection extends StatefulWidget { + const NavigationDrawerSection({super.key}); + + @override + State createState() => + _NavigationDrawerSectionState(); +} + +class _NavigationDrawerSectionState extends State { + int navDrawerIndex = 0; + + @override + Widget build(BuildContext context) { + return NavigationDrawer( + onDestinationSelected: (int selectedIndex) { + setState(() { + navDrawerIndex = selectedIndex; + }); + }, + selectedIndex: navDrawerIndex, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(28, 16, 16, 10), + child: Text( + 'Mail', + style: Theme.of(context).textTheme.titleSmall, + ), + ), + ...destinations.map((ExampleDestination destination) { + return NavigationDrawerDestination( + label: Text(destination.label), + icon: destination.icon, + selectedIcon: destination.selectedIcon, + ); + }), + const Divider(indent: 28, endIndent: 28), + Padding( + padding: const EdgeInsets.fromLTRB(28, 16, 16, 10), + child: Text( + 'Labels', + style: Theme.of(context).textTheme.titleSmall, + ), + ), + ...labelDestinations.map((ExampleDestination destination) { + return NavigationDrawerDestination( + label: Text(destination.label), + icon: destination.icon, + selectedIcon: destination.selectedIcon, + ); + }), + ], + ); + } +} + +class ExampleDestination { + const ExampleDestination(this.label, this.icon, this.selectedIcon); + + final String label; + final Widget icon; + final Widget selectedIcon; +} + +const List destinations = [ + ExampleDestination('Inbox', Icon(Icons.inbox_outlined), Icon(Icons.inbox)), + ExampleDestination('Outbox', Icon(Icons.send_outlined), Icon(Icons.send)), + ExampleDestination( + 'Favorites', Icon(Icons.favorite_outline), Icon(Icons.favorite)), + ExampleDestination('Trash', Icon(Icons.delete_outline), Icon(Icons.delete)), +]; + +const List labelDestinations = [ + ExampleDestination( + 'Family', Icon(Icons.bookmark_border), Icon(Icons.bookmark)), + ExampleDestination( + 'School', Icon(Icons.bookmark_border), Icon(Icons.bookmark)), + ExampleDestination('Work', Icon(Icons.bookmark_border), Icon(Icons.bookmark)), +]; + +class NavigationRails extends StatelessWidget { + const NavigationRails({super.key}); + + @override + Widget build(BuildContext context) { + return const ComponentDecoration( + label: 'Navigation rail', + tooltipMessage: 'Use NavigationRail', + child: IntrinsicWidth( + child: SizedBox(height: 420, child: NavigationRailSection())), + ); + } +} + +class NavigationRailSection extends StatefulWidget { + const NavigationRailSection({super.key}); + + @override + State createState() => _NavigationRailSectionState(); +} + +class _NavigationRailSectionState extends State { + int navRailIndex = 0; + + @override + Widget build(BuildContext context) { + return NavigationRail( + onDestinationSelected: (int selectedIndex) { + setState(() { + navRailIndex = selectedIndex; + }); + }, + elevation: 4, + leading: FloatingActionButton( + child: const Icon(Icons.create), onPressed: () {}), + groupAlignment: 0.0, + selectedIndex: navRailIndex, + labelType: NavigationRailLabelType.selected, + destinations: [ + ...destinations.map((ExampleDestination destination) { + return NavigationRailDestination( + label: Text(destination.label), + icon: destination.icon, + selectedIcon: destination.selectedIcon, + ); + }), + ], + ); + } +} + +class Tabs extends StatefulWidget { + const Tabs({super.key}); + + @override + State createState() => _TabsState(); +} + +class _TabsState extends State with TickerProviderStateMixin { + late TabController _tabController; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 3, vsync: this); + } + + @override + Widget build(BuildContext context) { + return ComponentDecoration( + label: 'Tabs', + tooltipMessage: 'Use TabBar', + child: SizedBox( + height: 80, + child: Scaffold( + appBar: AppBar( + bottom: TabBar( + controller: _tabController, + tabs: const [ + Tab( + icon: Icon(Icons.videocam_outlined), + text: 'Video', + iconMargin: EdgeInsets.zero, + ), + Tab( + icon: Icon(Icons.photo_outlined), + text: 'Photos', + iconMargin: EdgeInsets.zero, + ), + Tab( + icon: Icon(Icons.audiotrack_sharp), + text: 'Audio', + iconMargin: EdgeInsets.zero, + ), + ], + ), + ), + ), + ), + ); + } +} + +class TopAppBars extends StatelessWidget { + const TopAppBars({super.key}); + + static final List actions = [ + IconButton(icon: const Icon(Icons.attach_file), onPressed: () {}), + IconButton(icon: const Icon(Icons.event), onPressed: () {}), + IconButton(icon: const Icon(Icons.more_vert), onPressed: () {}), + ]; + + @override + Widget build(BuildContext context) { + return ComponentDecoration( + label: 'Top app bars', + tooltipMessage: + 'Use AppBar, SliverAppBar, SliverAppBar.medium, or SliverAppBar.large', + child: Column( + children: [ + AppBar( + title: const Text('Center-aligned'), + leading: const BackButton(), + actions: [ + IconButton( + iconSize: 32, + icon: const Icon(Icons.account_circle_outlined), + onPressed: () {}, + ), + ], + centerTitle: true, + ), + colDivider, + AppBar( + title: const Text('Small'), + leading: const BackButton(), + actions: actions, + centerTitle: false, + ), + colDivider, + SizedBox( + height: 100, + child: CustomScrollView( + slivers: [ + SliverAppBar.medium( + title: const Text('Medium'), + leading: const BackButton(), + actions: actions, + ), + const SliverFillRemaining(), + ], + ), + ), + colDivider, + SizedBox( + height: 130, + child: CustomScrollView( + slivers: [ + SliverAppBar.large( + title: const Text('Large'), + leading: const BackButton(), + actions: actions, + ), + const SliverFillRemaining(), + ], + ), + ), + ], + ), + ); + } +} + +class Menus extends StatefulWidget { + const Menus({super.key}); + + @override + State createState() => _MenusState(); +} + +class _MenusState extends State { + final TextEditingController colorController = TextEditingController(); + final TextEditingController iconController = TextEditingController(); + IconLabel? selectedIcon = IconLabel.smile; + ColorLabel? selectedColor; + + @override + Widget build(BuildContext context) { + final List> colorEntries = + >[]; + for (final ColorLabel color in ColorLabel.values) { + colorEntries.add(DropdownMenuEntry( + value: color, label: color.label, enabled: color.label != 'Grey')); + } + + final List> iconEntries = + >[]; + for (final IconLabel icon in IconLabel.values) { + iconEntries + .add(DropdownMenuEntry(value: icon, label: icon.label)); + } + + return ComponentDecoration( + label: 'Menus', + tooltipMessage: 'Use MenuAnchor or DropdownMenu', + child: Column( + children: [ + const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ButtonAnchorExample(), + rowDivider, + IconButtonAnchorExample(), + ], + ), + colDivider, + Wrap( + alignment: WrapAlignment.spaceAround, + runAlignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + spacing: smallSpacing, + runSpacing: smallSpacing, + children: [ + DropdownMenu( + controller: colorController, + label: const Text('Color'), + enableFilter: true, + dropdownMenuEntries: colorEntries, + inputDecorationTheme: const InputDecorationTheme(filled: true), + onSelected: (ColorLabel? color) { + setState(() { + selectedColor = color; + }); + }, + ), + DropdownMenu( + initialSelection: IconLabel.smile, + controller: iconController, + leadingIcon: const Icon(Icons.search), + label: const Text('Icon'), + dropdownMenuEntries: iconEntries, + onSelected: (IconLabel? icon) { + setState(() { + selectedIcon = icon; + }); + }, + ), + Icon( + selectedIcon?.icon, + color: selectedColor?.color ?? Colors.grey.withOpacity(0.5), + ) + ], + ), + ], + ), + ); + } +} + +enum ColorLabel { + blue('Blue', Colors.blue), + pink('Pink', Colors.pink), + green('Green', Colors.green), + yellow('Yellow', Colors.yellow), + grey('Grey', Colors.grey); + + const ColorLabel(this.label, this.color); + final String label; + final Color color; +} + +enum IconLabel { + smile('Smile', Icons.sentiment_satisfied_outlined), + cloud( + 'Cloud', + Icons.cloud_outlined, + ), + brush('Brush', Icons.brush_outlined), + heart('Heart', Icons.favorite); + + const IconLabel(this.label, this.icon); + final String label; + final IconData icon; +} + +class Sliders extends StatefulWidget { + const Sliders({super.key}); + + @override + State createState() => _SlidersState(); +} + +class _SlidersState extends State { + double sliderValue0 = 30.0; + double sliderValue1 = 20.0; + + @override + Widget build(BuildContext context) { + return ComponentDecoration( + label: 'Sliders', + tooltipMessage: 'Use Slider or RangeSlider', + child: Column( + children: [ + Slider( + max: 100, + value: sliderValue0, + onChanged: (double value) { + setState(() { + sliderValue0 = value; + }); + }, + ), + const SizedBox(height: 20), + Slider( + max: 100, + divisions: 5, + value: sliderValue1, + label: sliderValue1.round().toString(), + onChanged: (double value) { + setState(() { + sliderValue1 = value; + }); + }, + ), + ], + )); + } +} + +class ComponentDecoration extends StatefulWidget { + const ComponentDecoration({ + super.key, + required this.label, + required this.child, + this.tooltipMessage = '', + }); + + final String label; + final Widget child; + final String? tooltipMessage; + + @override + State createState() => _ComponentDecorationState(); +} + +class _ComponentDecorationState extends State { + final FocusNode focusNode = FocusNode(); + + @override + Widget build(BuildContext context) { + return RepaintBoundary( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: smallSpacing), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(widget.label, + style: Theme.of(context).textTheme.titleSmall), + Tooltip( + message: widget.tooltipMessage, + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 5.0), + child: Icon(Icons.info_outline, size: 16)), + ), + ], + ), + ConstrainedBox( + constraints: + const BoxConstraints.tightFor(width: widthConstraint), + // Tapping within the a component card should request focus + // for that component's children. + child: Focus( + focusNode: focusNode, + canRequestFocus: true, + child: GestureDetector( + onTapDown: (_) { + focusNode.requestFocus(); + }, + behavior: HitTestBehavior.opaque, + child: Card( + elevation: 0, + shape: RoundedRectangleBorder( + side: BorderSide( + color: Theme.of(context).colorScheme.outlineVariant, + ), + borderRadius: const BorderRadius.all(Radius.circular(12)), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 5.0, vertical: 20.0), + child: Center( + child: widget.child, + ), + ), + ), + ), + ), + ), + ], + ), + ), + ); + } +} + +class ComponentGroupDecoration extends StatelessWidget { + const ComponentGroupDecoration( + {super.key, required this.label, required this.children}); + + final String label; + final List children; + + @override + Widget build(BuildContext context) { + // Fully traverse this component group before moving on + return FocusTraversalGroup( + child: Card( + margin: EdgeInsets.zero, + elevation: 0, + color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.3), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 20.0), + child: Center( + child: Column( + children: [ + Text(label, style: Theme.of(context).textTheme.titleLarge), + colDivider, + ...children + ], + ), + ), + ), + ), + ); + } +} diff --git a/dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart b/dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart index 714e8f3f35d..fae34f30675 100644 --- a/dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart +++ b/dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart @@ -17,6 +17,7 @@ import 'src/web/bench_draw_rect.dart'; import 'src/web/bench_dynamic_clip_on_static_picture.dart'; import 'src/web/bench_image_decoding.dart'; import 'src/web/bench_link_infinite_scroll.dart'; +import 'src/web/bench_material_3.dart'; import 'src/web/bench_mouse_region_grid_hover.dart'; import 'src/web/bench_mouse_region_grid_scroll.dart'; import 'src/web/bench_mouse_region_mixed_grid_hover.dart'; @@ -60,6 +61,7 @@ final Map benchmarks = { BenchWrapBoxScroll.benchmarkName: () => BenchWrapBoxScroll(), BenchLinkInfiniteScroll.benchmarkName: () => BenchLinkInfiniteScroll.forward(), BenchLinkInfiniteScroll.benchmarkNameBackward: () => BenchLinkInfiniteScroll.backward(), + BenchMaterial3Components.benchmarkName: () => BenchMaterial3Components(), // CanvasKit-only benchmarks if (isCanvasKit) ...{