[iOS] Add spell check suggestions toolbar on tap (#119189)

[iOS] Add spell check suggestions toolbar on tap
This commit is contained in:
Camille Simon 2023-03-31 12:47:22 -07:00 committed by GitHub
parent 9d820aa35a
commit 52cacd9d1d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 393 additions and 64 deletions

View file

@ -58,6 +58,7 @@ export 'src/cupertino/search_field.dart';
export 'src/cupertino/segmented_control.dart';
export 'src/cupertino/slider.dart';
export 'src/cupertino/sliding_segmented_control.dart';
export 'src/cupertino/spell_check_suggestions_toolbar.dart';
export 'src/cupertino/switch.dart';
export 'src/cupertino/tab_scaffold.dart';
export 'src/cupertino/tab_view.dart';

View file

@ -0,0 +1,131 @@
// 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/scheduler.dart';
import 'package:flutter/services.dart' show SelectionChangedCause, SuggestionSpan;
import 'package:flutter/widgets.dart';
import 'text_selection_toolbar.dart';
import 'text_selection_toolbar_button.dart';
/// iOS only shows 3 spell check suggestions in the toolbar.
const int _maxSuggestions = 3;
/// The default spell check suggestions toolbar for iOS.
///
/// Tries to position itself below the [anchors], but if it doesn't fit, then it
/// readjusts to fit above bottom view insets.
class CupertinoSpellCheckSuggestionsToolbar extends StatelessWidget {
/// Constructs a [CupertinoSpellCheckSuggestionsToolbar].
const CupertinoSpellCheckSuggestionsToolbar({
super.key,
required this.anchors,
required this.buttonItems,
});
/// The location on which to anchor the menu.
final TextSelectionToolbarAnchors anchors;
/// 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.
/// * [SpellCheckSuggestionsToolbar.buttonItems], the list of
/// [ContextMenuButtonItem]s used to build the Material style spell check
/// suggestions toolbar.
final List<ContextMenuButtonItem> buttonItems;
/// 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;
}
if (spanAtCursorIndex.suggestions.isEmpty) {
return <ContextMenuButtonItem>[
ContextMenuButtonItem(
onPressed: () {},
label: 'No Replacements Found',
)
];
}
final List<ContextMenuButtonItem> buttonItems = <ContextMenuButtonItem>[];
// Build suggestion buttons.
int suggestionCount = 0;
for (final String suggestion in spanAtCursorIndex.suggestions) {
if (suggestionCount >= _maxSuggestions) {
break;
}
buttonItems.add(ContextMenuButtonItem(
onPressed: () {
if (!editableTextState.mounted) {
return;
}
_replaceText(
editableTextState,
suggestion,
spanAtCursorIndex.range,
);
},
label: suggestion,
));
suggestionCount += 1;
}
return buttonItems;
}
static void _replaceText(EditableTextState editableTextState, String text, TextRange replacementRange) {
// Replacement cannot be performed if the text is read only or obscured.
assert(!editableTextState.widget.readOnly && !editableTextState.widget.obscureText);
final TextEditingValue newValue = editableTextState.textEditingValue.replaced(
replacementRange,
text,
);
editableTextState.userUpdateTextEditingValue(newValue,SelectionChangedCause.toolbar);
// Schedule a call to bringIntoView() after renderEditable updates.
SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
if (editableTextState.mounted) {
editableTextState.bringIntoView(editableTextState.textEditingValue.selection.extent);
}
});
editableTextState.hideToolbar();
editableTextState.renderEditable.selectWordEdge(cause: SelectionChangedCause.toolbar);
}
/// Builds the toolbar buttons based on the [buttonItems].
List<Widget> _buildToolbarButtons(BuildContext context) {
return buttonItems.map((ContextMenuButtonItem buttonItem) {
return CupertinoTextSelectionToolbarButton.buttonItem(
buttonItem: buttonItem,
);
}).toList();
}
@override
Widget build(BuildContext context) {
final List<Widget> children = _buildToolbarButtons(context);
return CupertinoTextSelectionToolbar(
anchorAbove: anchors.primaryAnchor,
anchorBelow: anchors.secondaryAnchor == null ? anchors.primaryAnchor : anchors.secondaryAnchor!,
children: children,
);
}
}

View file

@ -15,6 +15,7 @@ import 'colors.dart';
import 'desktop_text_selection.dart';
import 'icons.dart';
import 'magnifier.dart';
import 'spell_check_suggestions_toolbar.dart';
import 'text_selection.dart';
import 'theme.dart';
@ -787,6 +788,32 @@ class CupertinoTextField extends StatefulWidget {
decorationStyle: TextDecorationStyle.dotted,
);
/// Default builder for the spell check suggestions toolbar in the Cupertino
/// style.
///
/// See also:
/// * [SpellCheckConfiguration.spellCheckSuggestionsToolbarBuilder], the
/// builder configured to show a spell check suggestions toolbar.
/// * [TextField.defaultSpellCheckSuggestionsToolbarBuilder], the builder
/// configured to show the Material style spell check suggestions toolbar.
@visibleForTesting
static Widget defaultSpellCheckSuggestionsToolbarBuilder(
BuildContext context,
EditableTextState editableTextState,
) {
final List<ContextMenuButtonItem>? buttonItems =
CupertinoSpellCheckSuggestionsToolbar.buildButtonItems(context, editableTextState);
if (buttonItems == null || buttonItems.isEmpty){
return const SizedBox.shrink();
}
return CupertinoSpellCheckSuggestionsToolbar(
anchors: editableTextState.contextMenuAnchors,
buttonItems: buttonItems,
);
}
/// {@macro flutter.widgets.undoHistory.controller}
final UndoHistoryController? undoController;
@ -1274,7 +1301,11 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
widget.spellCheckConfiguration != const SpellCheckConfiguration.disabled()
? widget.spellCheckConfiguration!.copyWith(
misspelledTextStyle: widget.spellCheckConfiguration!.misspelledTextStyle
?? CupertinoTextField.cupertinoMisspelledTextStyle)
?? CupertinoTextField.cupertinoMisspelledTextStyle,
spellCheckSuggestionsToolbarBuilder:
widget.spellCheckConfiguration!.spellCheckSuggestionsToolbarBuilder
?? CupertinoTextField.defaultSpellCheckSuggestionsToolbarBuilder,
)
: const SpellCheckConfiguration.disabled();
final Widget paddedEditable = Padding(

View file

@ -3,7 +3,8 @@
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart' show SuggestionSpan;
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart' show SelectionChangedCause, SuggestionSpan;
import 'adaptive_text_selection_toolbar.dart';
import 'colors.dart';
@ -42,6 +43,9 @@ class SpellCheckSuggestionsToolbar extends StatelessWidget {
/// * [AdaptiveTextSelectionToolbar.buttonItems], the list of
/// [ContextMenuButtonItem]s that are used to build the buttons of the
/// text selection toolbar.
/// * [CupertinoSpellCheckSuggestionsToolbar.buttonItems], the list of
/// [ContextMenuButtonItem]s used to build the Cupertino style spell check
/// suggestions toolbar.
final List<ContextMenuButtonItem> buttonItems;
/// Padding between the toolbar and the anchor. Eyeballed on Pixel 4 emulator
@ -77,10 +81,13 @@ class SpellCheckSuggestionsToolbar extends StatelessWidget {
for (final String suggestion in spanAtCursorIndex.suggestions) {
buttonItems.add(ContextMenuButtonItem(
onPressed: () {
editableTextState
.replaceComposingRegion(
SelectionChangedCause.toolbar,
suggestion,
if (!editableTextState.mounted) {
return;
}
_replaceText(
editableTextState,
suggestion,
spanAtCursorIndex.range,
);
},
label: suggestion,
@ -91,9 +98,13 @@ class SpellCheckSuggestionsToolbar extends StatelessWidget {
final ContextMenuButtonItem deleteButton =
ContextMenuButtonItem(
onPressed: () {
editableTextState.replaceComposingRegion(
SelectionChangedCause.toolbar,
if (!editableTextState.mounted) {
return;
}
_replaceText(
editableTextState,
'',
editableTextState.currentTextEditingValue.composing,
);
},
type: ContextMenuButtonType.delete,
@ -103,6 +114,25 @@ class SpellCheckSuggestionsToolbar extends StatelessWidget {
return buttonItems;
}
static void _replaceText(EditableTextState editableTextState, String text, TextRange replacementRange) {
// Replacement cannot be performed if the text is read only or obscured.
assert(!editableTextState.widget.readOnly && !editableTextState.widget.obscureText);
final TextEditingValue newValue = editableTextState.textEditingValue.replaced(
replacementRange,
text,
);
editableTextState.userUpdateTextEditingValue(newValue, SelectionChangedCause.toolbar);
// Schedule a call to bringIntoView() after renderEditable updates.
SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
if (editableTextState.mounted) {
editableTextState.bringIntoView(editableTextState.textEditingValue.selection.extent);
}
});
editableTextState.hideToolbar();
}
/// Determines the Offset that the toolbar will be anchored to.
static Offset getToolbarAnchor(TextSelectionToolbarAnchors anchors) {
return anchors.secondaryAnchor == null ? anchors.primaryAnchor : anchors.secondaryAnchor!;

View file

@ -805,7 +805,9 @@ class TextField extends StatefulWidget {
///
/// See also:
/// * [SpellCheckConfiguration.spellCheckSuggestionsToolbarBuilder], the
// builder configured to show a spell check suggestions toolbar.
/// builder configured to show a spell check suggestions toolbar.
/// * [CupertinoTextField.defaultSpellCheckSuggestionsToolbarBuilder], the builder
/// configured to show the Material style spell check suggestions toolbar.
@visibleForTesting
static Widget defaultSpellCheckSuggestionsToolbarBuilder(
BuildContext context,
@ -1239,7 +1241,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
?? TextField.materialMisspelledTextStyle,
spellCheckSuggestionsToolbarBuilder:
widget.spellCheckConfiguration!.spellCheckSuggestionsToolbarBuilder
?? TextField.defaultSpellCheckSuggestionsToolbarBuilder
?? TextField.defaultSpellCheckSuggestionsToolbarBuilder,
)
: const SpellCheckConfiguration.disabled();

View file

@ -15,9 +15,9 @@ import 'system_channels.dart';
/// to "Hello, wrold!" may be:
/// ```dart
/// SuggestionSpan suggestionSpan =
/// SuggestionSpan(
/// const TextRange(start: 7, end: 12),
/// List<String>.of(<String>['word', 'world', 'old']),
/// const SuggestionSpan(
/// TextRange(start: 7, end: 12),
/// <String>['word', 'world', 'old'],
/// );
/// ```
@immutable

View file

@ -2347,24 +2347,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
}
}
/// Replace composing region with specified text.
void replaceComposingRegion(SelectionChangedCause cause, String text) {
// Replacement cannot be performed if the text is read only or obscured.
assert(!widget.readOnly && !widget.obscureText);
_replaceText(ReplaceTextIntent(textEditingValue, text, textEditingValue.composing, cause));
if (cause == SelectionChangedCause.toolbar) {
// Schedule a call to bringIntoView() after renderEditable updates.
SchedulerBinding.instance.addPostFrameCallback((_) {
if (mounted) {
bringIntoView(textEditingValue.selection.extent);
}
});
hideToolbar();
}
}
/// Finds specified [SuggestionSpan] that matches the provided index using
/// binary search.
///
@ -3980,7 +3962,13 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
/// Shows toolbar with spell check suggestions of misspelled words that are
/// available for click-and-replace.
bool showSpellCheckSuggestionsToolbar() {
// Spell check suggestions toolbars are intended to be shown on non-web
// platforms. Additionally, the Cupertino style toolbar can't be drawn on
// the web with the HTML renderer due to
// https://github.com/flutter/flutter/issues/123560.
final bool platformNotSupported = kIsWeb && BrowserContextMenu.enabled;
if (!spellCheckEnabled
|| platformNotSupported
|| widget.readOnly
|| _selectionOverlay == null
|| !_spellCheckResultsReceived) {

View file

@ -2187,10 +2187,17 @@ class TextSelectionGestureDetectorBuilder {
case PointerDeviceKind.trackpad:
case PointerDeviceKind.stylus:
case PointerDeviceKind.invertedStylus:
// Precise devices should place the cursor at a precise position.
// TODO(camsim99): Determine spell check toolbar behavior in these cases:
// https://github.com/flutter/flutter/issues/119573.
// Precise devices should place the cursor at a precise position if the
// word at the text position is not misspelled.
renderEditable.selectPosition(cause: SelectionChangedCause.tap);
case PointerDeviceKind.touch:
case PointerDeviceKind.unknown:
// If the word that was tapped is misspelled, select the word and show the spell check suggestions
// toolbar once. If additional taps are made on a misspelled word, toggle the toolbar. If the word
// is not misspelled, default to the following behavior:
//
// Toggle the toolbar if the `previousSelection` is collapsed, the tap is on the selection, the
// TextAffinity remains the same, and the editable is focused. The TextAffinity is important when the
// cursor is on the boundary of a line wrap, if the affinity is different (i.e. it is downstream), the
@ -2205,9 +2212,17 @@ class TextSelectionGestureDetectorBuilder {
final TextSelection previousSelection = renderEditable.selection ?? editableText.textEditingValue.selection;
final TextPosition textPosition = renderEditable.getPositionForPoint(details.globalPosition);
final bool isAffinityTheSame = textPosition.affinity == previousSelection.affinity;
if (((_positionWasOnSelectionExclusive(textPosition) && !previousSelection.isCollapsed)
|| (_positionWasOnSelectionInclusive(textPosition) && previousSelection.isCollapsed && isAffinityTheSame))
&& renderEditable.hasFocus) {
final bool wordAtCursorIndexIsMisspelled = editableText.findSuggestionSpanAtCursorIndex(textPosition.offset) != null;
if (wordAtCursorIndexIsMisspelled) {
renderEditable.selectWord(cause: SelectionChangedCause.tap);
if (previousSelection != editableText.textEditingValue.selection) {
editableText.showSpellCheckSuggestionsToolbar();
} else {
editableText.toggleToolbar(false);
}
}
else if (((_positionWasOnSelectionExclusive(textPosition) && !previousSelection.isCollapsed) || (_positionWasOnSelectionInclusive(textPosition) && previousSelection.isCollapsed && isAffinityTheSame)) && renderEditable.hasFocus) {
editableText.toggleToolbar(false);
} else {
renderEditable.selectWordEdge(cause: SelectionChangedCause.tap);

View file

@ -15045,23 +15045,23 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async
selection: TextSelection(affinity: TextAffinity.upstream, baseOffset: 0, extentOffset: 4),
);
controller.value = value;
await tester.pumpWidget(
MaterialApp(
home: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
selectionControls: materialTextSelectionControls,
spellCheckConfiguration:
const SpellCheckConfiguration(
misspelledTextStyle: TextField.materialMisspelledTextStyle,
spellCheckSuggestionsToolbarBuilder: TextField.defaultSpellCheckSuggestionsToolbarBuilder,
),
),
await tester.pumpWidget(
MaterialApp(
home: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
selectionControls: materialTextSelectionControls,
spellCheckConfiguration:
const SpellCheckConfiguration(
misspelledTextStyle: TextField.materialMisspelledTextStyle,
spellCheckSuggestionsToolbarBuilder: TextField.defaultSpellCheckSuggestionsToolbarBuilder,
),
),
);
),
);
final EditableTextState state =
tester.state<EditableTextState>(find.byType(EditableText));
@ -15085,16 +15085,81 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async
from: Offset.zero,
cause: SelectionChangedCause.tap,
);
await tester.pumpAndSettle();
expect(state.showSpellCheckSuggestionsToolbar(), true);
// Toolbar will only show on non-web platforms.
expect(state.showSpellCheckSuggestionsToolbar(), !kIsWeb);
await tester.pumpAndSettle();
expect(find.text('test'), findsOneWidget);
expect(find.text('sets'), findsOneWidget);
expect(find.text('set'), findsOneWidget);
expect(find.text('DELETE'), findsOneWidget);
const Matcher matcher = kIsWeb ? findsNothing : findsOneWidget;
expect(find.text('test'), matcher);
expect(find.text('sets'), matcher);
expect(find.text('set'), matcher);
expect(find.text('DELETE'), matcher);
});
testWidgets('spell check suggestions toolbar buttons correctly change the composing region', (WidgetTester tester) async {
testWidgets('cupertino spell check suggestions toolbar buttons correctly change the composing region', (WidgetTester tester) async {
tester.binding.platformDispatcher.nativeSpellCheckServiceDefinedTestValue =
true;
const TextEditingValue value = TextEditingValue(
text: 'tset test test',
selection: TextSelection(affinity: TextAffinity.upstream, baseOffset: 0, extentOffset: 4),
);
controller.value = value;
await tester.pumpWidget(
CupertinoApp(
home: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
selectionControls: cupertinoTextSelectionControls,
spellCheckConfiguration:
const SpellCheckConfiguration(
misspelledTextStyle: CupertinoTextField.cupertinoMisspelledTextStyle,
spellCheckSuggestionsToolbarBuilder: CupertinoTextField.defaultSpellCheckSuggestionsToolbarBuilder,
),
),
),
);
final EditableTextState state =
tester.state<EditableTextState>(find.byType(EditableText));
state.spellCheckResults = const SpellCheckResults('tset test test', <SuggestionSpan>[SuggestionSpan(TextRange(start: 0, end: 4), <String>['test', 'sets', 'set'])]);
state.renderEditable.selectWordsInRange(
from: Offset.zero,
cause: SelectionChangedCause.tap,
);
await tester.pumpAndSettle();
// Set last tap down position so that selecting the word edge will be
// a valid operation.
final Offset pos1 = textOffsetToPosition(tester, 1);
final TestGesture gesture = await tester.startGesture(pos1);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(state.currentTextEditingValue.selection.baseOffset, equals(1));
// Test that tapping misspelled word replacement buttons will replace
// the correct word and select the word edge.
state.showSpellCheckSuggestionsToolbar();
await tester.pumpAndSettle();
if (kIsWeb) {
expect(find.text('sets'), findsNothing);
}
else {
expect(find.text('sets'), findsOneWidget);
await tester.tap(find.text('sets'));
await tester.pumpAndSettle();
expect(state.currentTextEditingValue.text, equals('sets test test'));
expect(state.currentTextEditingValue.selection.baseOffset, equals(4));
}
});
testWidgets('material spell check suggestions toolbar buttons correctly change the composing region', (WidgetTester tester) async {
tester.binding.platformDispatcher.nativeSpellCheckServiceDefinedTestValue =
true;
const TextEditingValue value = TextEditingValue(
@ -15129,21 +15194,34 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async
cause: SelectionChangedCause.tap,
);
await tester.pumpAndSettle();
expect(state.currentTextEditingValue.selection.baseOffset, equals(0));
// Test misspelled word replacement buttons.
state.showSpellCheckSuggestionsToolbar();
await tester.pumpAndSettle();
expect(find.text('sets'), findsOneWidget);
await tester.tap(find.text('sets'));
await tester.pumpAndSettle();
expect(state.currentTextEditingValue.text, equals('sets test test'));
if (kIsWeb) {
expect(find.text('sets'), findsNothing);
} else {
expect(find.text('sets'), findsOneWidget);
await tester.tap(find.text('sets'));
await tester.pumpAndSettle();
expect(state.currentTextEditingValue.text, equals('sets test test'));
expect(state.currentTextEditingValue.selection.baseOffset, equals(0));
}
// Test delete button.
state.showSpellCheckSuggestionsToolbar();
await tester.pumpAndSettle();
await tester.tap(find.text('DELETE'));
await tester.pumpAndSettle();
expect(state.currentTextEditingValue.text, equals(' test test'));
if (kIsWeb) {
expect(find.text('DELETE'), findsNothing);
} else {
expect(find.text('DELETE'), findsOneWidget);
await tester.tap(find.text('DELETE'));
await tester.pumpAndSettle();
expect(state.currentTextEditingValue.text, equals(' test test'));
expect(state.currentTextEditingValue.selection.baseOffset, equals(0));
}
});
});

View file

@ -12,6 +12,8 @@ import 'package:flutter_test/flutter_test.dart';
import 'clipboard_utils.dart';
import 'editable_text_utils.dart';
const int kSingleTapUpTimeout = 500;
void main() {
late int tapCount;
late int singleTapUpCount;
@ -688,6 +690,47 @@ void main() {
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }));
testWidgets('test TextSelectionGestureDetectorBuilder shows spell check toolbar on single tap on iOS if word misspelled and text selection toolbar on additonal taps', (WidgetTester tester) async {
await pumpTextSelectionGestureDetectorBuilder(tester);
final FakeEditableTextState state = tester.state(find.byType(FakeEditableText));
final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable));
const TextSelection selection = TextSelection.collapsed(offset: 1);
state.updateEditingValue(const TextEditingValue(text: 'something misspelled', selection: selection));
// Mark word to be tapped as misspelled for testing.
state.markCurrentSelectionAsMisspelled = true;
await tester.pump();
// Test spell check suggestions toolbar is shown on first tap of misspelled word.
const Offset position = Offset(25.0, 200.0);
await tester.tapAt(position);
await tester.pumpAndSettle();
expect(state.showSpellCheckSuggestionsToolbarCalled, isTrue);
// Reset and test text selection toolbar is toggled for additional taps.
state.showSpellCheckSuggestionsToolbarCalled = false;
renderEditable.selection = selection;
await tester.pump(const Duration(milliseconds: kSingleTapUpTimeout));
// Test first tap.
await tester.tapAt(position);
await tester.pumpAndSettle();
expect(state.showSpellCheckSuggestionsToolbarCalled, isFalse);
expect(state.toggleToolbarCalled, isTrue);
// Reset and test second tap.
state.toggleToolbarCalled = false;
await tester.pump(const Duration(milliseconds: kSingleTapUpTimeout));
await tester.tapAt(position);
await tester.pumpAndSettle();
expect(state.showSpellCheckSuggestionsToolbarCalled, isFalse);
expect(state.toggleToolbarCalled, isTrue);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }));
testWidgets('test TextSelectionGestureDetectorBuilder double tap', (WidgetTester tester) async {
await pumpTextSelectionGestureDetectorBuilder(tester);
final TestGesture gesture = await tester.startGesture(
@ -1646,6 +1689,7 @@ class FakeEditableTextState extends EditableTextState {
bool showToolbarCalled = false;
bool toggleToolbarCalled = false;
bool showSpellCheckSuggestionsToolbarCalled = false;
bool markCurrentSelectionAsMisspelled = false;
@override
RenderEditable get renderEditable => _editableKey.currentContext!.findRenderObject()! as RenderEditable;
@ -1668,6 +1712,15 @@ class FakeEditableTextState extends EditableTextState {
return true;
}
@override
SuggestionSpan? findSuggestionSpanAtCursorIndex(int cursorIndex) {
return markCurrentSelectionAsMisspelled
? const SuggestionSpan(
TextRange(start: 7, end: 12),
<String>['word', 'world', 'old'],
) : null;
}
@override
Widget build(BuildContext context) {
super.build(context);