// 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/cupertino.dart'; import 'package:flutter/services.dart' show SuggestionSpan; import 'adaptive_text_selection_toolbar.dart'; import 'colors.dart'; import 'material.dart'; import 'spell_check_suggestions_toolbar_layout_delegate.dart'; import 'text_selection_toolbar.dart'; import 'text_selection_toolbar_text_button.dart'; // The default height of the SpellCheckSuggestionsToolbar, which // assumes there are the maximum number of spell check suggestions available, 3. // Size eyeballed on Pixel 4 emulator running Android API 31. const double _kDefaultToolbarHeight = 193.0; /// The default spell check suggestions toolbar for Android. /// /// Tries to position itself below the [anchor], but if it doesn't fit, then it /// readjusts to fit above bottom view insets. class SpellCheckSuggestionsToolbar extends StatelessWidget { /// Constructs a [SpellCheckSuggestionsToolbar]. const SpellCheckSuggestionsToolbar({ super.key, required this.anchor, required this.buttonItems, }); /// {@template flutter.material.SpellCheckSuggestionsToolbar.anchor} /// The focal point below which the toolbar attempts to position itself. /// {@endtemplate} final Offset anchor; /// The [ContextMenuButtonItem]s that will be turned into the correct button /// widgets and displayed in the spell check suggestions toolbar. /// /// See also: /// /// * [AdaptiveTextSelectionToolbar.buttonItems], the list of /// [ContextMenuButtonItem]s that are used to build the buttons of the /// text selection toolbar. final List<ContextMenuButtonItem> buttonItems; /// Padding between the toolbar and the anchor. Eyeballed on Pixel 4 emulator /// running Android API 31. static const double kToolbarContentDistanceBelow = TextSelectionToolbar.kHandleSize - 3.0; /// Builds the default Android Material spell check suggestions toolbar. static Widget _spellCheckSuggestionsToolbarBuilder(BuildContext context, Widget child) { return _SpellCheckSuggestionsToolbarContainer( child: child, ); } /// Builds the button items for the toolbar based on the available /// spell check suggestions. static List<ContextMenuButtonItem>? buildButtonItems( BuildContext context, EditableTextState editableTextState, ) { // Determine if composing region is misspelled. final SuggestionSpan? spanAtCursorIndex = editableTextState.findSuggestionSpanAtCursorIndex( editableTextState.currentTextEditingValue.selection.baseOffset, ); if (spanAtCursorIndex == null) { return null; } final List<ContextMenuButtonItem> buttonItems = <ContextMenuButtonItem>[]; // Build suggestion buttons. for (final String suggestion in spanAtCursorIndex.suggestions) { buttonItems.add(ContextMenuButtonItem( onPressed: () { editableTextState .replaceComposingRegion( SelectionChangedCause.toolbar, suggestion, ); }, label: suggestion, )); } // Build delete button. final ContextMenuButtonItem deleteButton = ContextMenuButtonItem( onPressed: () { editableTextState.replaceComposingRegion( SelectionChangedCause.toolbar, '', ); }, type: ContextMenuButtonType.delete, ); buttonItems.add(deleteButton); return buttonItems; } /// Determines the Offset that the toolbar will be anchored to. static Offset getToolbarAnchor(TextSelectionToolbarAnchors anchors) { return anchors.secondaryAnchor == null ? anchors.primaryAnchor : anchors.secondaryAnchor!; } /// Builds the toolbar buttons based on the [buttonItems]. List<Widget> _buildToolbarButtons(BuildContext context) { return buttonItems.map((ContextMenuButtonItem buttonItem) { final TextSelectionToolbarTextButton button = TextSelectionToolbarTextButton( padding: const EdgeInsets.fromLTRB(20, 0, 0, 0), onPressed: buttonItem.onPressed, alignment: Alignment.centerLeft, child: Text( AdaptiveTextSelectionToolbar.getButtonLabel(context, buttonItem), style: buttonItem.type == ContextMenuButtonType.delete ? const TextStyle(color: Colors.blue) : null, ), ); if (buttonItem.type != ContextMenuButtonType.delete) { return button; } return DecoratedBox( decoration: const BoxDecoration(border: Border(top: BorderSide(color: Colors.grey))), child: button, ); }).toList(); } @override Widget build(BuildContext context) { // Adjust toolbar height if needed. final double spellCheckSuggestionsToolbarHeight = _kDefaultToolbarHeight - (48.0 * (4 - buttonItems.length)); // Incorporate the padding distance between the content and toolbar. final Offset anchorPadded = anchor + const Offset(0.0, kToolbarContentDistanceBelow); final MediaQueryData mediaQueryData = MediaQuery.of(context); final double softKeyboardViewInsetsBottom = mediaQueryData.viewInsets.bottom; final double paddingAbove = mediaQueryData.padding.top + CupertinoTextSelectionToolbar.kToolbarScreenPadding; // Makes up for the Padding. final Offset localAdjustment = Offset(CupertinoTextSelectionToolbar.kToolbarScreenPadding, paddingAbove); return Padding( padding: EdgeInsets.fromLTRB( CupertinoTextSelectionToolbar.kToolbarScreenPadding, kToolbarContentDistanceBelow, CupertinoTextSelectionToolbar.kToolbarScreenPadding, CupertinoTextSelectionToolbar.kToolbarScreenPadding + softKeyboardViewInsetsBottom, ), child: CustomSingleChildLayout( delegate: SpellCheckSuggestionsToolbarLayoutDelegate( anchor: anchorPadded - localAdjustment, ), child: AnimatedSize( // This duration was eyeballed on a Pixel 2 emulator running Android // API 28 for the Material TextSelectionToolbar. duration: const Duration(milliseconds: 140), child: _spellCheckSuggestionsToolbarBuilder(context, _SpellCheckSuggestsionsToolbarItemsLayout( height: spellCheckSuggestionsToolbarHeight, children: <Widget>[..._buildToolbarButtons(context)], )), ), ), ); } } /// The Material-styled toolbar outline for the spell check suggestions /// toolbar. class _SpellCheckSuggestionsToolbarContainer extends StatelessWidget { const _SpellCheckSuggestionsToolbarContainer({ required this.child, }); final Widget child; @override Widget build(BuildContext context) { return Material( // This elevation was eyeballed on a Pixel 4 emulator running Android // API 31 for the SpellCheckSuggestionsToolbar. elevation: 2.0, type: MaterialType.card, child: child, ); } } /// Renders the spell check suggestions toolbar items in the correct positions /// in the menu. class _SpellCheckSuggestsionsToolbarItemsLayout extends StatelessWidget { const _SpellCheckSuggestsionsToolbarItemsLayout({ required this.height, required this.children, }); final double height; final List<Widget> children; @override Widget build(BuildContext context) { return SizedBox( // This width was eyeballed on a Pixel 4 emulator running Android // API 31 for the SpellCheckSuggestionsToolbar. width: 165, height: height, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: children, ), ); } }