diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index 26565c63fc..a2586cd056 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -697,15 +697,6 @@ class EditableText extends StatefulWidget { /// context, the English phrase will be on the right and the Hebrew phrase on /// its left. /// - /// When LTR text is entered into an RTL field, or RTL text is entered into an - /// LTR field, [LRM](https://en.wikipedia.org/wiki/Left-to-right_mark) or - /// [RLM](https://en.wikipedia.org/wiki/Right-to-left_mark) characters will be - /// inserted alongside whitespace characters, respectively. This is to - /// eliminate ambiguous directionality in whitespace and ensure proper caret - /// placement. These characters will affect the length of the string and may - /// need to be parsed out when doing things like string comparison with other - /// text. - /// /// Defaults to the ambient [Directionality], if any. /// {@endtemplate} final TextDirection? textDirection; @@ -2243,8 +2234,6 @@ class EditableTextState extends State with AutomaticKeepAliveClien _lastBottomViewInset = WidgetsBinding.instance!.window.viewInsets.bottom; } - late final _WhitespaceDirectionalityFormatter _whitespaceFormatter = _WhitespaceDirectionalityFormatter(textDirection: _textDirection); - void _formatAndSetValue(TextEditingValue value, SelectionChangedCause? cause, {bool userInteraction = false}) { // Only apply input formatters if the text has changed (including uncommited // text in the composing region), or when the user committed the composing @@ -2263,14 +2252,6 @@ class EditableTextState extends State with AutomaticKeepAliveClien value, (TextEditingValue newValue, TextInputFormatter formatter) => formatter.formatEditUpdate(_value, newValue), ) ?? value; - - // Always pass the text through the whitespace directionality formatter to - // maintain expected behavior with carets on trailing whitespace. - // TODO(LongCatIsLooong): The if statement here is for retaining the - // previous behavior. The input formatter logic will be updated in an - // upcoming PR. - if (widget.inputFormatters?.isNotEmpty ?? false) - value = _whitespaceFormatter.formatEditUpdate(_value, value); } // Put all optional user callback invocations in a batch edit to prevent @@ -2885,184 +2866,3 @@ class _Editable extends LeafRenderObjectWidget { ..setPromptRectRange(promptRectRange); } } - -// This formatter inserts [Unicode.RLM] and [Unicode.LRM] into the -// string in order to preserve expected caret behavior when trailing -// whitespace is inserted. -// -// When typing in a direction that opposes the base direction -// of the paragraph, un-enclosed whitespace gets the directionality -// of the paragraph. This is often at odds with what is immediately -// being typed causing the caret to jump to the wrong side of the text. -// This formatter makes use of the RLM and LRM to cause the text -// shaper to inherently treat the whitespace as being surrounded -// by the directionality of the previous non-whitespace codepoint. -class _WhitespaceDirectionalityFormatter extends TextInputFormatter { - // The [textDirection] should be the base directionality of the - // paragraph/editable. - _WhitespaceDirectionalityFormatter({TextDirection? textDirection}) - : _baseDirection = textDirection, - _previousNonWhitespaceDirection = textDirection; - - // Using regex here instead of ICU is suboptimal, but is enough - // to produce the correct results for any reasonable input where this - // is even relevant. Using full ICU would be a much heavier change, - // requiring exposure of the C++ ICU API. - // - // LTR covers most scripts and symbols, including but not limited to Latin, - // ideographic scripts (Chinese, Japanese, etc), Cyrilic, Indic, and - // SE Asian scripts. - final RegExp _ltrRegExp = RegExp(r'[A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02B8\u0300-\u0590\u0800-\u1FFF\u2C00-\uFB1C\uFDFE-\uFE6F\uFEFD-\uFFFF]'); - // RTL covers Arabic, Hebrew, and other RTL languages such as Urdu, - // Aramic, Farsi, Dhivehi. - final RegExp _rtlRegExp = RegExp(r'[\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC]'); - // Although whitespaces are not the only codepoints that have weak directionality, - // these are the primary cause of the caret being misplaced. - final RegExp _whitespaceRegExp = RegExp(r'\s'); - - final TextDirection? _baseDirection; - // Tracks the directionality of the most recently encountered - // codepoint that was not whitespace. This becomes the direction of - // marker inserted to fully surround ambiguous whitespace. - TextDirection? _previousNonWhitespaceDirection; - - // Prevents the formatter from attempting more expensive formatting - // operations mixed directionality is found. - bool _hasOpposingDirection = false; - - // See [Unicode.RLM] and [Unicode.LRM]. - // - // We do not directly use the [Unicode] constants since they are strings. - static const int _rlm = 0x200F; - static const int _lrm = 0x200E; - - @override - TextEditingValue formatEditUpdate( - TextEditingValue oldValue, - TextEditingValue newValue, - ) { - // Skip formatting (which can be more expensive) if there are no cases of - // mixing directionality. Once a case of mixed directionality is found, - // always perform the formatting. - if (!_hasOpposingDirection) { - _hasOpposingDirection = _baseDirection == TextDirection.ltr ? - _rtlRegExp.hasMatch(newValue.text) : _ltrRegExp.hasMatch(newValue.text); - } - - if (_hasOpposingDirection) { - _previousNonWhitespaceDirection = _baseDirection; - - final List outputCodepoints = []; - - // We add/subtract from these as we insert/remove markers. - int selectionBase = newValue.selection.baseOffset; - int selectionExtent = newValue.selection.extentOffset; - int composingStart = newValue.composing.start; - int composingEnd = newValue.composing.end; - - void addToLength() { - selectionBase += outputCodepoints.length <= selectionBase ? 1 : 0; - selectionExtent += outputCodepoints.length <= selectionExtent ? 1 : 0; - - composingStart += outputCodepoints.length <= composingStart ? 1 : 0; - composingEnd += outputCodepoints.length <= composingEnd ? 1 : 0; - } - void subtractFromLength() { - selectionBase -= outputCodepoints.length < selectionBase ? 1 : 0; - selectionExtent -= outputCodepoints.length < selectionExtent ? 1 : 0; - - composingStart -= outputCodepoints.length < composingStart ? 1 : 0; - composingEnd -= outputCodepoints.length < composingEnd ? 1 : 0; - } - - final bool isBackspace = oldValue.text.runes.length - newValue.text.runes.length == 1 && - isDirectionalityMarker(oldValue.text.runes.last) && - oldValue.text.substring(0, oldValue.text.length - 1) == newValue.text; - - bool previousWasWhitespace = false; - bool previousWasDirectionalityMarker = false; - int? previousNonWhitespaceCodepoint; - int index = 0; - for (final int codepoint in newValue.text.runes) { - if (isWhitespace(codepoint)) { - // Only compute the directionality of the non-whitespace - // when the value is needed. - if (!previousWasWhitespace && previousNonWhitespaceCodepoint != null) { - _previousNonWhitespaceDirection = getDirection(previousNonWhitespaceCodepoint); - } - // If we already added directionality for this run of whitespace, - // "shift" the marker added to the end of the whitespace run. - if (previousWasWhitespace) { - subtractFromLength(); - outputCodepoints.removeLast(); - } - // Handle trailing whitespace deleting the directionality char instead of the whitespace. - if (isBackspace && index == newValue.text.runes.length - 1) { - // Do not append the whitespace to the outputCodepoints. - subtractFromLength(); - } else { - outputCodepoints.add(codepoint); - addToLength(); - outputCodepoints.add(_previousNonWhitespaceDirection == TextDirection.rtl ? _rlm : _lrm); - } - - previousWasWhitespace = true; - previousWasDirectionalityMarker = false; - } else if (isDirectionalityMarker(codepoint)) { - // Handle pre-existing directionality markers. Use pre-existing marker - // instead of the one we add. - if (previousWasWhitespace) { - subtractFromLength(); - outputCodepoints.removeLast(); - } - outputCodepoints.add(codepoint); - - previousWasWhitespace = false; - previousWasDirectionalityMarker = true; - } else { - // If the whitespace was already enclosed by the same directionality, - // we can remove the artificially added marker. - if (!previousWasDirectionalityMarker && - previousWasWhitespace && - getDirection(codepoint) == _previousNonWhitespaceDirection) { - subtractFromLength(); - outputCodepoints.removeLast(); - } - // Normal character, track its codepoint add it to the string. - previousNonWhitespaceCodepoint = codepoint; - outputCodepoints.add(codepoint); - - previousWasWhitespace = false; - previousWasDirectionalityMarker = false; - } - index++; - } - final String formatted = String.fromCharCodes(outputCodepoints); - return TextEditingValue( - text: formatted, - selection: TextSelection( - baseOffset: selectionBase, - extentOffset: selectionExtent, - affinity: newValue.selection.affinity, - isDirectional: newValue.selection.isDirectional - ), - composing: TextRange(start: composingStart, end: composingEnd), - ); - } - return newValue; - } - - bool isWhitespace(int value) { - return _whitespaceRegExp.hasMatch(String.fromCharCode(value)); - } - - bool isDirectionalityMarker(int value) { - return value == _rlm || value == _lrm; - } - - TextDirection getDirection(int value) { - // Use the LTR version as short-circuiting will be more efficient since - // there are more LTR codepoints. - return _ltrRegExp.hasMatch(String.fromCharCode(value)) ? TextDirection.ltr : TextDirection.rtl; - } -} diff --git a/packages/flutter/test/widgets/editable_text_test.dart b/packages/flutter/test/widgets/editable_text_test.dart index e60ab76ef5..9a1a33f30b 100644 --- a/packages/flutter/test/widgets/editable_text_test.dart +++ b/packages/flutter/test/widgets/editable_text_test.dart @@ -121,6 +121,62 @@ void main() { equals(serializedActionName)); } + // Regression test for https://github.com/flutter/flutter/issues/34538. + testWidgets('RTL arabic correct caret placement after trailing whitespace', (WidgetTester tester) async { + final TextEditingController controller = TextEditingController(); + await tester.pumpWidget( + MediaQuery( + data: const MediaQueryData(), + child: Directionality( + textDirection: TextDirection.rtl, + child: FocusScope( + node: focusScopeNode, + autofocus: true, + child: EditableText( + backgroundCursorColor: Colors.blue, + controller: controller, + focusNode: focusNode, + maxLines: 1, // Sets text keyboard implicitly. + style: textStyle, + cursorColor: cursorColor, + ), + ), + ), + ), + ); + + await tester.tap(find.byType(EditableText)); + await tester.showKeyboard(find.byType(EditableText)); + await tester.idle(); + + final EditableTextState state = tester.state(find.byType(EditableText)); + + // Simulates Gboard Persian input. + state.updateEditingValue(const TextEditingValue(text: 'گ', selection: TextSelection.collapsed(offset: 1))); + await tester.pump(); + double previousCaretXPosition = state.renderEditable.getLocalRectForCaret(state.textEditingValue.selection.base).left; + + state.updateEditingValue(const TextEditingValue(text: 'گی', selection: TextSelection.collapsed(offset: 2))); + await tester.pump(); + double caretXPosition = state.renderEditable.getLocalRectForCaret(state.textEditingValue.selection.base).left; + expect(caretXPosition, lessThan(previousCaretXPosition)); + previousCaretXPosition = caretXPosition; + + state.updateEditingValue(const TextEditingValue(text: 'گیگ', selection: TextSelection.collapsed(offset: 3))); + await tester.pump(); + caretXPosition = state.renderEditable.getLocalRectForCaret(state.textEditingValue.selection.base).left; + expect(caretXPosition, lessThan(previousCaretXPosition)); + previousCaretXPosition = caretXPosition; + + // Enter a whitespace in a RTL input field moves the caret to the left. + state.updateEditingValue(const TextEditingValue(text: 'گیگ ', selection: TextSelection.collapsed(offset: 4))); + await tester.pump(); + caretXPosition = state.renderEditable.getLocalRectForCaret(state.textEditingValue.selection.base).left; + expect(caretXPosition, lessThan(previousCaretXPosition)); + + expect(state.currentTextEditingValue.text, equals('گیگ ')); + }, skip: isBrowser); // https://github.com/flutter/flutter/issues/78550. + testWidgets('has expected defaults', (WidgetTester tester) async { await tester.pumpWidget( MediaQuery( @@ -6136,271 +6192,6 @@ void main() { expect(formatter.lastOldValue.text, 'test'); }); - testWidgets('Whitespace directionality formatter input Arabic', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(text: 'testText'); - await tester.pumpWidget( - MediaQuery( - data: const MediaQueryData(devicePixelRatio: 1.0), - child: Directionality( - textDirection: TextDirection.ltr, - child: FocusScope( - node: focusScopeNode, - autofocus: true, - child: EditableText( - backgroundCursorColor: Colors.blue, - controller: controller, - focusNode: focusNode, - maxLines: 1, // Sets text keyboard implicitly. - style: textStyle, - cursorColor: cursorColor, - ), - ), - ), - ), - ); - - await tester.tap(find.byType(EditableText)); - await tester.showKeyboard(find.byType(EditableText)); - controller.text = ''; - await tester.idle(); - - final EditableTextState state = - tester.state(find.byType(EditableText)); - expect(tester.testTextInput.editingState!['text'], equals('')); - expect(state.wantKeepAlive, true); - - // Simple mixed directional input. - state.updateEditingValue(const TextEditingValue(text: 'h')); - state.updateEditingValue(const TextEditingValue(text: 'he')); - state.updateEditingValue(const TextEditingValue(text: 'hel')); - state.updateEditingValue(const TextEditingValue(text: 'hell')); - state.updateEditingValue(const TextEditingValue(text: 'hello')); - expect(state.currentTextEditingValue.text, equals('hello')); - state.updateEditingValue(const TextEditingValue(text: 'hello ', composing: TextRange(start: 4, end: 5))); - expect(state.currentTextEditingValue.text, equals('hello ')); - state.updateEditingValue(const TextEditingValue(text: 'hello ا', composing: TextRange(start: 4, end: 6))); - expect(state.currentTextEditingValue.text, equals('hello \u{200E}ا')); - expect(state.currentTextEditingValue.composing, equals(const TextRange(start: 4, end: 7))); - state.updateEditingValue(const TextEditingValue(text: 'hello الْ', composing: TextRange(start: 4, end: 7))); - state.updateEditingValue(const TextEditingValue(text: 'hello الْعَ', composing: TextRange(start: 4, end: 8))); - state.updateEditingValue(const TextEditingValue(text: 'hello الْعَ ', composing: TextRange(start: 4, end: 9))); - expect(state.currentTextEditingValue.text, equals('hello \u{200E}الْعَ \u{200F}')); - expect(state.currentTextEditingValue.composing, equals(const TextRange(start: 4, end: 10))); - state.updateEditingValue(const TextEditingValue(text: 'hello الْعَ بِيَّةُ')); - state.updateEditingValue(const TextEditingValue(text: 'hello الْعَ بِيَّةُ ')); - expect(state.currentTextEditingValue.text, equals('hello \u{200E}الْعَ بِيَّةُ \u{200F}')); - }); - - testWidgets('Whitespace directionality formatter doesn\'t overwrite existing Arabic', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(text: 'testText'); - await tester.pumpWidget( - MediaQuery( - data: const MediaQueryData(devicePixelRatio: 1.0), - child: Directionality( - textDirection: TextDirection.ltr, - child: FocusScope( - node: focusScopeNode, - autofocus: true, - child: EditableText( - backgroundCursorColor: Colors.blue, - controller: controller, - focusNode: focusNode, - maxLines: 1, // Sets text keyboard implicitly. - style: textStyle, - cursorColor: cursorColor, - ), - ), - ), - ), - ); - - await tester.tap(find.byType(EditableText)); - await tester.showKeyboard(find.byType(EditableText)); - controller.text = ''; - await tester.idle(); - - final EditableTextState state = - tester.state(find.byType(EditableText)); - expect(tester.testTextInput.editingState!['text'], equals('')); - expect(state.wantKeepAlive, true); - - // Does not overwrite existing RLM or LRM characters - state.updateEditingValue(const TextEditingValue(text: 'hello \u{200F}ا')); - expect(state.currentTextEditingValue.text, equals('hello \u{200F}ا')); - state.updateEditingValue(const TextEditingValue(text: 'hello \u{200F}ا \u{200E}ا ا ')); - expect(state.currentTextEditingValue.text, equals('hello \u{200F}ا \u{200E}ا ا \u{200F}')); - - // Handles only directionality markers. - state.updateEditingValue(const TextEditingValue(text: '\u{200E}\u{200F}')); - expect(state.currentTextEditingValue.text, equals('\u{200E}\u{200F}')); - state.updateEditingValue(const TextEditingValue(text: '\u{200E}\u{200F}\u{200E}\u{200F}\u{200E}\u{200F}')); - expect(state.currentTextEditingValue.text, equals('\u{200E}\u{200F}\u{200E}\u{200F}\u{200E}\u{200F}')); - state.updateEditingValue(const TextEditingValue(text: '\u{200E}\u{200F}\u{200F}\u{200F}')); - expect(state.currentTextEditingValue.text, equals('\u{200E}\u{200F}\u{200F}\u{200F}')); - }); - - testWidgets('Whitespace directionality formatter is not leaky Arabic', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(text: 'testText'); - await tester.pumpWidget( - MediaQuery( - data: const MediaQueryData(devicePixelRatio: 1.0), - child: Directionality( - textDirection: TextDirection.ltr, - child: FocusScope( - node: focusScopeNode, - autofocus: true, - child: EditableText( - backgroundCursorColor: Colors.blue, - controller: controller, - focusNode: focusNode, - maxLines: 1, // Sets text keyboard implicitly. - style: textStyle, - cursorColor: cursorColor, - ), - ), - ), - ), - ); - - await tester.tap(find.byType(EditableText)); - await tester.showKeyboard(find.byType(EditableText)); - controller.text = ''; - await tester.idle(); - - final EditableTextState state = - tester.state(find.byType(EditableText)); - expect(tester.testTextInput.editingState!['text'], equals('')); - expect(state.wantKeepAlive, true); - - // Can be passed through formatter repeatedly without leaking/growing. - state.updateEditingValue(const TextEditingValue(text: 'hello \u{200E}عَ \u{200F}عَ \u{200F}عَ \u{200F}')); - expect(state.currentTextEditingValue.text, equals('hello \u{200E}عَ \u{200F}عَ \u{200F}عَ \u{200F}')); - state.updateEditingValue(const TextEditingValue(text: 'hello \u{200E}عَ \u{200F}عَ \u{200F}عَ \u{200F}')); - expect(state.currentTextEditingValue.text, equals('hello \u{200E}عَ \u{200F}عَ \u{200F}عَ \u{200F}')); - state.updateEditingValue(const TextEditingValue(text: 'hello \u{200E}عَ \u{200F}عَ \u{200F}عَ \u{200F}')); - expect(state.currentTextEditingValue.text, equals('hello \u{200E}عَ \u{200F}عَ \u{200F}عَ \u{200F}')); - }); - - testWidgets('Whitespace directionality formatter emojis', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(text: 'testText'); - await tester.pumpWidget( - MediaQuery( - data: const MediaQueryData(devicePixelRatio: 1.0), - child: Directionality( - textDirection: TextDirection.ltr, - child: FocusScope( - node: focusScopeNode, - autofocus: true, - child: EditableText( - backgroundCursorColor: Colors.blue, - controller: controller, - focusNode: focusNode, - maxLines: 1, // Sets text keyboard implicitly. - style: textStyle, - cursorColor: cursorColor, - ), - ), - ), - ), - ); - - await tester.tap(find.byType(EditableText)); - await tester.showKeyboard(find.byType(EditableText)); - controller.text = ''; - await tester.idle(); - - final EditableTextState state = - tester.state(find.byType(EditableText)); - expect(tester.testTextInput.editingState!['text'], equals('')); - expect(state.wantKeepAlive, true); - - // Doesn't eat emojis - state.updateEditingValue(const TextEditingValue(text: '\u{200E}😀😁😂🤣😃 💑 👩‍❤️‍👩 👨‍❤️‍👨 💏 👩‍❤️‍💋‍👩 👨‍❤️‍💋‍👨 👪 👨‍👩‍👧 👨‍👩‍👧‍👦 👨‍👩‍👦‍👦 \u{200F}')); - expect(state.currentTextEditingValue.text, equals('\u{200E}😀😁😂🤣😃 💑 👩‍❤️‍👩 👨‍❤️‍👨 💏 👩‍❤️‍💋‍👩 👨‍❤️‍💋‍👨 👪 👨‍👩‍👧 👨‍👩‍👧‍👦 👨‍👩‍👦‍👦 \u{200F}')); - state.updateEditingValue(const TextEditingValue(text: '\u{200E}🇧🇼🇧🇷🇮🇴 🇻🇬🇧🇳wahhh!🇧🇬🇧🇫 🇧🇮🇰🇭عَ عَ 🇨🇲 🇨🇦🇮🇨 🇨🇻🇧🇶 🇰🇾🇨🇫 🇹🇩🇨🇱 🇨🇳🇨🇽\u{200F}')); - expect(state.currentTextEditingValue.text, equals('\u{200E}🇧🇼🇧🇷🇮🇴 🇻🇬🇧🇳wahhh!🇧🇬🇧🇫 🇧🇮🇰🇭عَ عَ \u{200F}🇨🇲 🇨🇦🇮🇨 🇨🇻🇧🇶 🇰🇾🇨🇫 🇹🇩🇨🇱 🇨🇳🇨🇽\u{200F}')); - }); - - testWidgets('Whitespace directionality formatter emojis', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(text: 'testText'); - await tester.pumpWidget( - MediaQuery( - data: const MediaQueryData(devicePixelRatio: 1.0), - child: Directionality( - textDirection: TextDirection.ltr, - child: FocusScope( - node: focusScopeNode, - autofocus: true, - child: EditableText( - backgroundCursorColor: Colors.blue, - controller: controller, - focusNode: focusNode, - maxLines: 1, // Sets text keyboard implicitly. - style: textStyle, - cursorColor: cursorColor, - ), - ), - ), - ), - ); - - await tester.tap(find.byType(EditableText)); - await tester.showKeyboard(find.byType(EditableText)); - controller.text = ''; - await tester.idle(); - - final EditableTextState state = - tester.state(find.byType(EditableText)); - expect(tester.testTextInput.editingState!['text'], equals('')); - expect(state.wantKeepAlive, true); - - // Doesn't eat emojis - state.updateEditingValue(const TextEditingValue(text: '\u{200E}😀😁😂🤣😃 💑 👩‍❤️‍👩 👨‍❤️‍👨 💏 👩‍❤️‍💋‍👩 👨‍❤️‍💋‍👨 👪 👨‍👩‍👧 👨‍👩‍👧‍👦 👨‍👩‍👦‍👦 \u{200F}')); - expect(state.currentTextEditingValue.text, equals('\u{200E}😀😁😂🤣😃 💑 👩‍❤️‍👩 👨‍❤️‍👨 💏 👩‍❤️‍💋‍👩 👨‍❤️‍💋‍👨 👪 👨‍👩‍👧 👨‍👩‍👧‍👦 👨‍👩‍👦‍👦 \u{200F}')); - state.updateEditingValue(const TextEditingValue(text: '\u{200E}🇧🇼🇧🇷🇮🇴 🇻🇬🇧🇳wahhh!🇧🇬🇧🇫 🇧🇮🇰🇭عَ عَ 🇨🇲 🇨🇦🇮🇨 🇨🇻🇧🇶 🇰🇾🇨🇫 🇹🇩🇨🇱 🇨🇳🇨🇽\u{200F}')); - expect(state.currentTextEditingValue.text, equals('\u{200E}🇧🇼🇧🇷🇮🇴 🇻🇬🇧🇳wahhh!🇧🇬🇧🇫 🇧🇮🇰🇭عَ عَ \u{200F}🇨🇲 🇨🇦🇮🇨 🇨🇻🇧🇶 🇰🇾🇨🇫 🇹🇩🇨🇱 🇨🇳🇨🇽\u{200F}')); - }); - - testWidgets('Whitespace directionality formatter handles deletion of trailing whitespace', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(text: 'testText'); - await tester.pumpWidget( - MediaQuery( - data: const MediaQueryData(devicePixelRatio: 1.0), - child: Directionality( - textDirection: TextDirection.ltr, - child: FocusScope( - node: focusScopeNode, - autofocus: true, - child: EditableText( - backgroundCursorColor: Colors.blue, - controller: controller, - focusNode: focusNode, - maxLines: 1, // Sets text keyboard implicitly. - style: textStyle, - cursorColor: cursorColor, - ), - ), - ), - ), - ); - - await tester.tap(find.byType(EditableText)); - await tester.showKeyboard(find.byType(EditableText)); - controller.text = ''; - await tester.idle(); - - final EditableTextState state = - tester.state(find.byType(EditableText)); - expect(tester.testTextInput.editingState!['text'], equals('')); - expect(state.wantKeepAlive, true); - - // Simulate deleting only the trailing RTL mark. - state.updateEditingValue(const TextEditingValue(text: 'hello \u{200E}الْعَ بِيَّةُ \u{200F}')); - state.updateEditingValue(const TextEditingValue(text: 'hello \u{200E}الْعَ بِيَّةُ ')); - // The trailing space should be gone here. - expect(state.currentTextEditingValue.text, equals('hello \u{200E}الْعَ بِيَّةُ')); - }); - testWidgets('EditableText changes mouse cursor when hovered', (WidgetTester tester) async { await tester.pumpWidget( MediaQuery(