// 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. // TODO(Mathias): Make a PR in Flutter repository that enables customizing // the dropdown menu without having to copy the entire file. // This is a temporary solution! import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; const double _kMinimumWidth = 112.0; const double _kDefaultHorizontalPadding = 12.0; // Navigation shortcuts to move the selected menu items up or down. final Map _kMenuTraversalShortcuts = { LogicalKeySet(LogicalKeyboardKey.arrowUp): const _ArrowUpIntent(), LogicalKeySet(LogicalKeyboardKey.arrowDown): const _ArrowDownIntent(), }; /// A dropdown menu that can be opened from a [TextField]. The selected /// menu item is displayed in that field. /// /// This widget is used to help people make a choice from a menu and put the /// selected item into the text input field. People can also filter the list based /// on the text input or search one item in the menu list. /// /// The menu is composed of a list of [DropdownMenuEntry]s. People can provide information, /// such as: label, leading icon or trailing icon for each entry. The [TextField] /// will be updated based on the selection from the menu entries. The text field /// will stay empty if the selected entry is disabled. /// /// The dropdown menu can be traversed by pressing the up or down key. During the /// process, the corresponding item will be highlighted and the text field will be updated. /// Disabled items will be skipped during traversal. /// /// The menu can be scrollable if not all items in the list are displayed at once. /// /// {@tool dartpad} /// This sample shows how to display outlined [AFDropdownMenu] and filled [AFDropdownMenu]. /// /// ** See code in examples/api/lib/material/dropdown_menu/dropdown_menu.0.dart ** /// {@end-tool} /// /// See also: /// /// * [MenuAnchor], which is a widget used to mark the "anchor" for a set of submenus. /// The [AFDropdownMenu] uses a [TextField] as the "anchor". /// * [TextField], which is a text input widget that uses an [InputDecoration]. /// * [DropdownMenuEntry], which is used to build the [MenuItemButton] in the [AFDropdownMenu] list. class AFDropdownMenu extends StatefulWidget { /// Creates a const [AFDropdownMenu]. /// /// The leading and trailing icons in the text field can be customized by using /// [leadingIcon], [trailingIcon] and [selectedTrailingIcon] properties. They are /// passed down to the [InputDecoration] properties, and will override values /// in the [InputDecoration.prefixIcon] and [InputDecoration.suffixIcon]. /// /// Except leading and trailing icons, the text field can be configured by the /// [InputDecorationTheme] property. The menu can be configured by the [menuStyle]. const AFDropdownMenu({ super.key, this.enabled = true, this.width, this.menuHeight, this.leadingIcon, this.trailingIcon, this.label, this.hintText, this.helperText, this.errorText, this.selectedTrailingIcon, this.enableFilter = false, this.enableSearch = true, this.textStyle, this.inputDecorationTheme, this.menuStyle, this.controller, this.initialSelection, this.onSelected, this.requestFocusOnTap, this.expandedInsets, this.searchCallback, required this.dropdownMenuEntries, }); /// Determine if the [AFDropdownMenu] is enabled. /// /// Defaults to true. final bool enabled; /// Determine the width of the [AFDropdownMenu]. /// /// If this is null, the width of the [AFDropdownMenu] will be the same as the width of the widest /// menu item plus the width of the leading/trailing icon. final double? width; /// Determine the height of the menu. /// /// If this is null, the menu will display as many items as possible on the screen. final double? menuHeight; /// An optional Icon at the front of the text input field. /// /// Defaults to null. If this is not null, the menu items will have extra paddings to be aligned /// with the text in the text field. final Widget? leadingIcon; /// An optional icon at the end of the text field. /// /// Defaults to an [Icon] with [Icons.arrow_drop_down]. final Widget? trailingIcon; /// Optional widget that describes the input field. /// /// When the input field is empty and unfocused, the label is displayed on /// top of the input field (i.e., at the same location on the screen where /// text may be entered in the input field). When the input field receives /// focus (or if the field is non-empty), the label moves above, either /// vertically adjacent to, or to the center of the input field. /// /// Defaults to null. final Widget? label; /// Text that suggests what sort of input the field accepts. /// /// Defaults to null; final String? hintText; /// Text that provides context about the [AFDropdownMenu]'s value, such /// as how the value will be used. /// /// If non-null, the text is displayed below the input field, in /// the same location as [errorText]. If a non-null [errorText] value is /// specified then the helper text is not shown. /// /// Defaults to null; /// /// See also: /// /// * [InputDecoration.helperText], which is the text that provides context about the [InputDecorator.child]'s value. final String? helperText; /// Text that appears below the input field and the border to show the error message. /// /// If non-null, the border's color animates to red and the [helperText] is not shown. /// /// Defaults to null; /// /// See also: /// /// * [InputDecoration.errorText], which is the text that appears below the [InputDecorator.child] and the border. final String? errorText; /// An optional icon at the end of the text field to indicate that the text /// field is pressed. /// /// Defaults to an [Icon] with [Icons.arrow_drop_up]. final Widget? selectedTrailingIcon; /// Determine if the menu list can be filtered by the text input. /// /// Defaults to false. final bool enableFilter; /// Determine if the first item that matches the text input can be highlighted. /// /// Defaults to true as the search function could be commonly used. final bool enableSearch; /// The text style for the [TextField] of the [AFDropdownMenu]; /// /// Defaults to the overall theme's [TextTheme.bodyLarge] /// if the dropdown menu theme's value is null. final TextStyle? textStyle; /// Defines the default appearance of [InputDecoration] to show around the text field. /// /// By default, shows a outlined text field. final InputDecorationTheme? inputDecorationTheme; /// The [MenuStyle] that defines the visual attributes of the menu. /// /// The default width of the menu is set to the width of the text field. final MenuStyle? menuStyle; /// Controls the text being edited or selected in the menu. /// /// If null, this widget will create its own [TextEditingController]. final TextEditingController? controller; /// The value used to for an initial selection. /// /// Defaults to null. final T? initialSelection; /// The callback is called when a selection is made. /// /// Defaults to null. If null, only the text field is updated. final ValueChanged? onSelected; /// Determine if the dropdown button requests focus and the on-screen virtual /// keyboard is shown in response to a touch event. /// /// By default, on mobile platforms, tapping on the text field and opening /// the menu will not cause a focus request and the virtual keyboard will not /// appear. The default behavior for desktop platforms is for the dropdown to /// take the focus. /// /// Defaults to null. Setting this field to true or false, rather than allowing /// the implementation to choose based on the platform, can be useful for /// applications that want to override the default behavior. final bool? requestFocusOnTap; /// Descriptions of the menu items in the [AFDropdownMenu]. /// /// This is a required parameter. It is recommended that at least one [DropdownMenuEntry] /// is provided. If this is an empty list, the menu will be empty and only /// contain space for padding. final List> dropdownMenuEntries; /// Defines the menu text field's width to be equal to its parent's width /// plus the horizontal width of the specified insets. /// /// If this property is null, the width of the text field will be determined /// by the width of menu items or [AFDropdownMenu.width]. If this property is not null, /// the text field's width will match the parent's width plus the specified insets. /// If the value of this property is [EdgeInsets.zero], the width of the text field will be the same /// as its parent's width. /// /// The [expandedInsets]' top and bottom are ignored, only its left and right /// properties are used. /// /// Defaults to null. final EdgeInsets? expandedInsets; /// When [AFDropdownMenu.enableSearch] is true, this callback is used to compute /// the index of the search result to be highlighted. /// /// {@tool snippet} /// /// In this example the `searchCallback` returns the index of the search result /// that exactly matches the query. /// /// ```dart /// DropdownMenu( /// searchCallback: (List> entries, String query) { /// if (query.isEmpty) { /// return null; /// } /// final int index = entries.indexWhere((DropdownMenuEntry entry) => entry.label == query); /// /// return index != -1 ? index : null; /// }, /// dropdownMenuEntries: const >[], /// ) /// ``` /// {@end-tool} /// /// Defaults to null. If this is null and [AFDropdownMenu.enableSearch] is true, /// the default function will return the index of the first matching result /// which contains the contents of the text input field. final SearchCallback? searchCallback; @override State> createState() => _AFDropdownMenuState(); } class _AFDropdownMenuState extends State> { final GlobalKey _anchorKey = GlobalKey(); final GlobalKey _leadingKey = GlobalKey(); late List buttonItemKeys; final MenuController _controller = MenuController(); late bool _enableFilter; late List> filteredEntries; List? _initialMenu; int? currentHighlight; double? leadingPadding; bool _menuHasEnabledItem = false; TextEditingController? _localTextEditingController; TextEditingController get _textEditingController { return widget.controller ?? (_localTextEditingController ??= TextEditingController()); } @override void initState() { super.initState(); _enableFilter = widget.enableFilter; filteredEntries = widget.dropdownMenuEntries; buttonItemKeys = List.generate( filteredEntries.length, (int index) => GlobalKey(), ); _menuHasEnabledItem = filteredEntries.any((DropdownMenuEntry entry) => entry.enabled); final int index = filteredEntries.indexWhere( (DropdownMenuEntry entry) => entry.value == widget.initialSelection, ); if (index != -1) { _textEditingController.value = TextEditingValue( text: filteredEntries[index].label, selection: TextSelection.collapsed( offset: filteredEntries[index].label.length, ), ); } refreshLeadingPadding(); } @override void dispose() { _localTextEditingController?.dispose(); _localTextEditingController = null; super.dispose(); } @override void didUpdateWidget(AFDropdownMenu oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.controller != widget.controller) { if (widget.controller != null) { _localTextEditingController?.dispose(); _localTextEditingController = null; } } if (oldWidget.enableSearch != widget.enableSearch) { if (!widget.enableSearch) { currentHighlight = null; } } if (oldWidget.dropdownMenuEntries != widget.dropdownMenuEntries) { currentHighlight = null; filteredEntries = widget.dropdownMenuEntries; buttonItemKeys = List.generate( filteredEntries.length, (int index) => GlobalKey(), ); _menuHasEnabledItem = filteredEntries.any((DropdownMenuEntry entry) => entry.enabled); } if (oldWidget.leadingIcon != widget.leadingIcon) { refreshLeadingPadding(); } if (oldWidget.initialSelection != widget.initialSelection) { final int index = filteredEntries.indexWhere( (DropdownMenuEntry entry) => entry.value == widget.initialSelection, ); if (index != -1) { _textEditingController.value = TextEditingValue( text: filteredEntries[index].label, selection: TextSelection.collapsed( offset: filteredEntries[index].label.length, ), ); } } } bool canRequestFocus() { if (widget.requestFocusOnTap != null) { return widget.requestFocusOnTap!; } switch (Theme.of(context).platform) { case TargetPlatform.iOS: case TargetPlatform.android: case TargetPlatform.fuchsia: return false; case TargetPlatform.macOS: case TargetPlatform.linux: case TargetPlatform.windows: return true; } } void refreshLeadingPadding() { WidgetsBinding.instance.addPostFrameCallback( (_) { setState(() { leadingPadding = getWidth(_leadingKey); }); }, debugLabel: 'DropdownMenu.refreshLeadingPadding', ); } void scrollToHighlight() { WidgetsBinding.instance.addPostFrameCallback( (_) { final BuildContext? highlightContext = buttonItemKeys[currentHighlight!].currentContext; if (highlightContext != null) { Scrollable.ensureVisible(highlightContext); } }, debugLabel: 'DropdownMenu.scrollToHighlight', ); } double? getWidth(GlobalKey key) { final BuildContext? context = key.currentContext; if (context != null) { final RenderBox box = context.findRenderObject()! as RenderBox; return box.hasSize ? box.size.width : null; } return null; } List> filter( List> entries, TextEditingController textEditingController, ) { final String filterText = textEditingController.text.toLowerCase(); return entries .where( (DropdownMenuEntry entry) => entry.label.toLowerCase().contains(filterText), ) .toList(); } int? search( List> entries, TextEditingController textEditingController, ) { final String searchText = textEditingController.value.text.toLowerCase(); if (searchText.isEmpty) { return null; } final int index = entries.indexWhere( (DropdownMenuEntry entry) => entry.label.toLowerCase().contains(searchText), ); return index != -1 ? index : null; } List _buildButtons( List> filteredEntries, TextDirection textDirection, { int? focusedIndex, bool enableScrollToHighlight = true, }) { final List result = []; for (int i = 0; i < filteredEntries.length; i++) { final DropdownMenuEntry entry = filteredEntries[i]; // By default, when the text field has a leading icon but a menu entry doesn't // have one, the label of the entry should have extra padding to be aligned // with the text in the text input field. When both the text field and the // menu entry have leading icons, the menu entry should remove the extra // paddings so its leading icon will be aligned with the leading icon of // the text field. final double padding = entry.leadingIcon == null ? (leadingPadding ?? _kDefaultHorizontalPadding) : _kDefaultHorizontalPadding; final ButtonStyle defaultStyle; switch (textDirection) { case TextDirection.rtl: defaultStyle = MenuItemButton.styleFrom( padding: EdgeInsets.only( left: _kDefaultHorizontalPadding, right: padding, ), ); case TextDirection.ltr: defaultStyle = MenuItemButton.styleFrom( padding: EdgeInsets.only( left: padding, right: _kDefaultHorizontalPadding, ), ); } ButtonStyle effectiveStyle = entry.style ?? defaultStyle; final Color focusedBackgroundColor = effectiveStyle.foregroundColor ?.resolve({WidgetState.focused}) ?? Theme.of(context).colorScheme.onSurface; Widget label = entry.labelWidget ?? Text(entry.label); if (widget.width != null) { final double horizontalPadding = padding + _kDefaultHorizontalPadding; label = ConstrainedBox( constraints: BoxConstraints(maxWidth: widget.width! - horizontalPadding), child: label, ); } // Simulate the focused state because the text field should always be focused // during traversal. If the menu item has a custom foreground color, the "focused" // color will also change to foregroundColor.withOpacity(0.12). effectiveStyle = entry.enabled && i == focusedIndex ? effectiveStyle.copyWith( backgroundColor: WidgetStatePropertyAll( focusedBackgroundColor.withOpacity(0.12), ), ) : effectiveStyle; final Widget menuItemButton = Padding( padding: const EdgeInsets.only(bottom: 6), child: MenuItemButton( key: enableScrollToHighlight ? buttonItemKeys[i] : null, style: effectiveStyle, leadingIcon: entry.leadingIcon, trailingIcon: entry.trailingIcon, onPressed: entry.enabled ? () { _textEditingController.value = TextEditingValue( text: entry.label, selection: TextSelection.collapsed(offset: entry.label.length), ); currentHighlight = widget.enableSearch ? i : null; widget.onSelected?.call(entry.value); } : null, requestFocusOnHover: false, child: label, ), ); result.add(menuItemButton); } return result; } void handleUpKeyInvoke(_) { setState(() { if (!_menuHasEnabledItem || !_controller.isOpen) { return; } _enableFilter = false; currentHighlight ??= 0; currentHighlight = (currentHighlight! - 1) % filteredEntries.length; while (!filteredEntries[currentHighlight!].enabled) { currentHighlight = (currentHighlight! - 1) % filteredEntries.length; } final String currentLabel = filteredEntries[currentHighlight!].label; _textEditingController.value = TextEditingValue( text: currentLabel, selection: TextSelection.collapsed(offset: currentLabel.length), ); }); } void handleDownKeyInvoke(_) { setState(() { if (!_menuHasEnabledItem || !_controller.isOpen) { return; } _enableFilter = false; currentHighlight ??= -1; currentHighlight = (currentHighlight! + 1) % filteredEntries.length; while (!filteredEntries[currentHighlight!].enabled) { currentHighlight = (currentHighlight! + 1) % filteredEntries.length; } final String currentLabel = filteredEntries[currentHighlight!].label; _textEditingController.value = TextEditingValue( text: currentLabel, selection: TextSelection.collapsed(offset: currentLabel.length), ); }); } void handlePressed(MenuController controller) { if (controller.isOpen) { currentHighlight = null; controller.close(); } else { // close to open if (_textEditingController.text.isNotEmpty) { _enableFilter = false; } controller.open(); } setState(() {}); } @override Widget build(BuildContext context) { final TextDirection textDirection = Directionality.of(context); _initialMenu ??= _buildButtons( widget.dropdownMenuEntries, textDirection, enableScrollToHighlight: false, ); final DropdownMenuThemeData theme = DropdownMenuTheme.of(context); final DropdownMenuThemeData defaults = _DropdownMenuDefaultsM3(context); if (_enableFilter) { filteredEntries = filter(widget.dropdownMenuEntries, _textEditingController); } if (widget.enableSearch) { if (widget.searchCallback != null) { currentHighlight = widget.searchCallback! .call(filteredEntries, _textEditingController.text); } else { currentHighlight = search(filteredEntries, _textEditingController); } if (currentHighlight != null) { scrollToHighlight(); } } final List menu = _buildButtons( filteredEntries, textDirection, focusedIndex: currentHighlight, ); final TextStyle? effectiveTextStyle = widget.textStyle ?? theme.textStyle ?? defaults.textStyle; MenuStyle? effectiveMenuStyle = widget.menuStyle ?? theme.menuStyle ?? defaults.menuStyle!; final double? anchorWidth = getWidth(_anchorKey); if (widget.width != null) { effectiveMenuStyle = effectiveMenuStyle.copyWith( minimumSize: WidgetStatePropertyAll(Size(widget.width!, 0.0)), ); } else if (anchorWidth != null) { effectiveMenuStyle = effectiveMenuStyle.copyWith( minimumSize: WidgetStatePropertyAll(Size(anchorWidth, 0.0)), ); } if (widget.menuHeight != null) { effectiveMenuStyle = effectiveMenuStyle.copyWith( maximumSize: WidgetStatePropertyAll( Size(double.infinity, widget.menuHeight!), ), ); } final InputDecorationTheme effectiveInputDecorationTheme = widget.inputDecorationTheme ?? theme.inputDecorationTheme ?? defaults.inputDecorationTheme!; final MouseCursor effectiveMouseCursor = canRequestFocus() ? SystemMouseCursors.text : SystemMouseCursors.click; Widget menuAnchor = MenuAnchor( style: effectiveMenuStyle, controller: _controller, menuChildren: menu, crossAxisUnconstrained: false, builder: ( BuildContext context, MenuController controller, Widget? child, ) { assert(_initialMenu != null); final Widget trailingButton = Padding( padding: const EdgeInsets.all(4.0), child: IconButton( splashRadius: 1, isSelected: controller.isOpen, icon: widget.trailingIcon ?? const Icon(Icons.arrow_drop_down), selectedIcon: widget.selectedTrailingIcon ?? const Icon(Icons.arrow_drop_up), onPressed: () { handlePressed(controller); }, ), ); final Widget leadingButton = Padding( padding: const EdgeInsets.all(8.0), child: widget.leadingIcon ?? const SizedBox(), ); final Widget textField = TextField( key: _anchorKey, mouseCursor: effectiveMouseCursor, canRequestFocus: canRequestFocus(), enableInteractiveSelection: canRequestFocus(), textAlignVertical: TextAlignVertical.center, style: effectiveTextStyle, controller: _textEditingController, onEditingComplete: () { if (currentHighlight != null) { final DropdownMenuEntry entry = filteredEntries[currentHighlight!]; if (entry.enabled) { _textEditingController.value = TextEditingValue( text: entry.label, selection: TextSelection.collapsed(offset: entry.label.length), ); widget.onSelected?.call(entry.value); } } else { widget.onSelected?.call(null); } if (!widget.enableSearch) { currentHighlight = null; } controller.close(); }, onTap: () { handlePressed(controller); }, onChanged: (String text) { controller.open(); setState(() { filteredEntries = widget.dropdownMenuEntries; _enableFilter = widget.enableFilter; }); }, decoration: InputDecoration( enabled: widget.enabled, label: widget.label, hintText: widget.hintText, helperText: widget.helperText, errorText: widget.errorText, prefixIcon: widget.leadingIcon != null ? Container(key: _leadingKey, child: widget.leadingIcon) : null, suffixIcon: trailingButton, ).applyDefaults(effectiveInputDecorationTheme), ); if (widget.expandedInsets != null) { // If [expandedInsets] is not null, the width of the text field should depend // on its parent width. So we don't need to use `_DropdownMenuBody` to // calculate the children's width. return textField; } return _DropdownMenuBody( width: widget.width, children: [ textField, for (final Widget item in _initialMenu!) item, trailingButton, leadingButton, ], ); }, ); if (widget.expandedInsets != null) { menuAnchor = Container( alignment: AlignmentDirectional.topStart, padding: widget.expandedInsets?.copyWith(top: 0.0, bottom: 0.0), child: menuAnchor, ); } return Shortcuts( shortcuts: _kMenuTraversalShortcuts, child: Actions( actions: >{ _ArrowUpIntent: CallbackAction<_ArrowUpIntent>( onInvoke: handleUpKeyInvoke, ), _ArrowDownIntent: CallbackAction<_ArrowDownIntent>( onInvoke: handleDownKeyInvoke, ), }, child: menuAnchor, ), ); } } class _ArrowUpIntent extends Intent { const _ArrowUpIntent(); } class _ArrowDownIntent extends Intent { const _ArrowDownIntent(); } class _DropdownMenuBody extends MultiChildRenderObjectWidget { const _DropdownMenuBody({ super.children, this.width, }); final double? width; @override _RenderDropdownMenuBody createRenderObject(BuildContext context) { return _RenderDropdownMenuBody( width: width, ); } @override void updateRenderObject( BuildContext context, _RenderDropdownMenuBody renderObject, ) { renderObject.width = width; } } class _DropdownMenuBodyParentData extends ContainerBoxParentData {} class _RenderDropdownMenuBody extends RenderBox with ContainerRenderObjectMixin, RenderBoxContainerDefaultsMixin { _RenderDropdownMenuBody({ double? width, }) : _width = width; double? get width => _width; double? _width; set width(double? value) { if (_width == value) { return; } _width = value; markNeedsLayout(); } @override void setupParentData(RenderBox child) { if (child.parentData is! _DropdownMenuBodyParentData) { child.parentData = _DropdownMenuBodyParentData(); } } @override void performLayout() { final BoxConstraints constraints = this.constraints; double maxWidth = 0.0; double? maxHeight; RenderBox? child = firstChild; final BoxConstraints innerConstraints = BoxConstraints( maxWidth: width ?? computeMaxIntrinsicWidth(constraints.maxWidth), maxHeight: computeMaxIntrinsicHeight(constraints.maxHeight), ); while (child != null) { if (child == firstChild) { child.layout(innerConstraints, parentUsesSize: true); maxHeight ??= child.size.height; final _DropdownMenuBodyParentData childParentData = child.parentData! as _DropdownMenuBodyParentData; assert(child.parentData == childParentData); child = childParentData.nextSibling; continue; } child.layout(innerConstraints, parentUsesSize: true); final _DropdownMenuBodyParentData childParentData = child.parentData! as _DropdownMenuBodyParentData; childParentData.offset = Offset.zero; maxWidth = math.max(maxWidth, child.size.width); maxHeight ??= child.size.height; assert(child.parentData == childParentData); child = childParentData.nextSibling; } assert(maxHeight != null); maxWidth = math.max(_kMinimumWidth, maxWidth); size = constraints.constrain(Size(width ?? maxWidth, maxHeight!)); } @override void paint(PaintingContext context, Offset offset) { final RenderBox? child = firstChild; if (child != null) { final _DropdownMenuBodyParentData childParentData = child.parentData! as _DropdownMenuBodyParentData; context.paintChild(child, offset + childParentData.offset); } } @override Size computeDryLayout(BoxConstraints constraints) { final BoxConstraints constraints = this.constraints; double maxWidth = 0.0; double? maxHeight; RenderBox? child = firstChild; final BoxConstraints innerConstraints = BoxConstraints( maxWidth: width ?? computeMaxIntrinsicWidth(constraints.maxWidth), maxHeight: computeMaxIntrinsicHeight(constraints.maxHeight), ); while (child != null) { if (child == firstChild) { final Size childSize = child.getDryLayout(innerConstraints); maxHeight ??= childSize.height; final _DropdownMenuBodyParentData childParentData = child.parentData! as _DropdownMenuBodyParentData; assert(child.parentData == childParentData); child = childParentData.nextSibling; continue; } final Size childSize = child.getDryLayout(innerConstraints); final _DropdownMenuBodyParentData childParentData = child.parentData! as _DropdownMenuBodyParentData; childParentData.offset = Offset.zero; maxWidth = math.max(maxWidth, childSize.width); maxHeight ??= childSize.height; assert(child.parentData == childParentData); child = childParentData.nextSibling; } assert(maxHeight != null); maxWidth = math.max(_kMinimumWidth, maxWidth); return constraints.constrain(Size(width ?? maxWidth, maxHeight!)); } @override double computeMinIntrinsicWidth(double height) { RenderBox? child = firstChild; double width = 0; while (child != null) { if (child == firstChild) { final _DropdownMenuBodyParentData childParentData = child.parentData! as _DropdownMenuBodyParentData; child = childParentData.nextSibling; continue; } final double maxIntrinsicWidth = child.getMinIntrinsicWidth(height); if (child == lastChild) { width += maxIntrinsicWidth; } if (child == childBefore(lastChild!)) { width += maxIntrinsicWidth; } width = math.max(width, maxIntrinsicWidth); final _DropdownMenuBodyParentData childParentData = child.parentData! as _DropdownMenuBodyParentData; child = childParentData.nextSibling; } return math.max(width, _kMinimumWidth); } @override double computeMaxIntrinsicWidth(double height) { RenderBox? child = firstChild; double width = 0; while (child != null) { if (child == firstChild) { final _DropdownMenuBodyParentData childParentData = child.parentData! as _DropdownMenuBodyParentData; child = childParentData.nextSibling; continue; } final double maxIntrinsicWidth = child.getMaxIntrinsicWidth(height); // Add the width of leading Icon. if (child == lastChild) { width += maxIntrinsicWidth; } // Add the width of trailing Icon. if (child == childBefore(lastChild!)) { width += maxIntrinsicWidth; } width = math.max(width, maxIntrinsicWidth); final _DropdownMenuBodyParentData childParentData = child.parentData! as _DropdownMenuBodyParentData; child = childParentData.nextSibling; } return math.max(width, _kMinimumWidth); } @override double computeMinIntrinsicHeight(double height) { final RenderBox? child = firstChild; double width = 0; if (child != null) { width = math.max(width, child.getMinIntrinsicHeight(height)); } return width; } @override double computeMaxIntrinsicHeight(double height) { final RenderBox? child = firstChild; double width = 0; if (child != null) { width = math.max(width, child.getMaxIntrinsicHeight(height)); } return width; } @override bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { final RenderBox? child = firstChild; if (child != null) { final _DropdownMenuBodyParentData childParentData = child.parentData! as _DropdownMenuBodyParentData; final bool isHit = result.addWithPaintOffset( offset: childParentData.offset, position: position, hitTest: (BoxHitTestResult result, Offset transformed) { assert(transformed == position - childParentData.offset); return child.hitTest(result, position: transformed); }, ); if (isHit) { return true; } } return false; } } // Hand coded defaults. These will be updated once we have tokens/spec. class _DropdownMenuDefaultsM3 extends DropdownMenuThemeData { _DropdownMenuDefaultsM3(this.context); final BuildContext context; late final ThemeData _theme = Theme.of(context); @override TextStyle? get textStyle => _theme.textTheme.bodyLarge; @override MenuStyle get menuStyle { return const MenuStyle( minimumSize: WidgetStatePropertyAll(Size(_kMinimumWidth, 0.0)), maximumSize: WidgetStatePropertyAll(Size.infinite), visualDensity: VisualDensity.standard, ); } @override InputDecorationTheme get inputDecorationTheme { return const InputDecorationTheme(border: OutlineInputBorder()); } }