diff --git a/packages/flutter/lib/src/rendering/editable.dart b/packages/flutter/lib/src/rendering/editable.dart index 02eda54e18..86c3ed2b2c 100644 --- a/packages/flutter/lib/src/rendering/editable.dart +++ b/packages/flutter/lib/src/rendering/editable.dart @@ -564,6 +564,21 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { } void _setSelection(TextSelection nextSelection, SelectionChangedCause cause) { + if (nextSelection.isValid) { + // The nextSelection is calculated based on _plainText, which can be out + // of sync with the textSelectionDelegate.textEditingValue by one frame. + // This is due to the render editable and editable text handle pointer + // event separately. If the editable text changes the text during the + // event handler, the render editable will use the outdated text stored in + // the _plainText when handling the pointer event. + // + // If this happens, we need to make sure the new selection is still valid. + final int textLength = textSelectionDelegate.textEditingValue.text.length; + nextSelection = nextSelection.copyWith( + baseOffset: math.min(nextSelection.baseOffset, textLength), + extentOffset: math.min(nextSelection.extentOffset, textLength), + ); + } _handleSelectionChange(nextSelection, cause); _setTextEditingValue( textSelectionDelegate.textEditingValue.copyWith(selection: nextSelection), diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index ee46a93a36..857ddd03d8 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -444,6 +444,33 @@ void main() { expect(renderEditable.cursorColor!.alpha, 0); }, variant: const TargetPlatformVariant({ TargetPlatform.iOS, TargetPlatform.macOS })); + // Regression test for https://github.com/flutter/flutter/issues/78918. + testWidgets('RenderEditable sets correct text editing value', (WidgetTester tester) async { + final TextEditingController controller = TextEditingController(text: 'how are you'); + final UniqueKey icon = UniqueKey(); + await tester.pumpWidget( + MaterialApp( + home: Material( + child: TextField( + controller: controller, + decoration: InputDecoration( + suffixIcon: IconButton( + key: icon, + icon: const Icon(Icons.cancel), + onPressed: () => controller.clear(), + ), + ), + ), + ), + ), + ); + + await tester.tap(find.byKey(icon)); + await tester.pump(); + expect(controller.text, ''); + expect(controller.selection, const TextSelection.collapsed(offset: 0, affinity: TextAffinity.upstream)); + }); + testWidgets('Cursor radius is 2.0', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( diff --git a/packages/flutter/test/rendering/editable_test.dart b/packages/flutter/test/rendering/editable_test.dart index bd2875ddd1..1081703795 100644 --- a/packages/flutter/test/rendering/editable_test.dart +++ b/packages/flutter/test/rendering/editable_test.dart @@ -413,7 +413,9 @@ void main() { }); test('selects correct place with offsets', () { - final TextSelectionDelegate delegate = FakeEditableTextState(); + const String text = 'test\ntest'; + final TextSelectionDelegate delegate = FakeEditableTextState() + ..textEditingValue = const TextEditingValue(text: text); final ViewportOffset viewportOffset = ViewportOffset.zero(); late TextSelection currentSelection; final RenderEditable editable = RenderEditable( @@ -431,7 +433,7 @@ void main() { startHandleLayerLink: LayerLink(), endHandleLayerLink: LayerLink(), text: const TextSpan( - text: 'test\ntest', + text: text, style: TextStyle( height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', ), @@ -498,7 +500,9 @@ void main() { }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61026 test('selects correct place when offsets are flipped', () { - final TextSelectionDelegate delegate = FakeEditableTextState(); + const String text = 'abc def ghi'; + final TextSelectionDelegate delegate = FakeEditableTextState() + ..textEditingValue = const TextEditingValue(text: text); final ViewportOffset viewportOffset = ViewportOffset.zero(); late TextSelection currentSelection; final RenderEditable editable = RenderEditable( @@ -512,7 +516,7 @@ void main() { currentSelection = selection; }, text: const TextSpan( - text: 'abc def ghi', + text: text, style: TextStyle( height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', ), @@ -534,9 +538,11 @@ void main() { test('selection does not flicker as user is dragging', () { int selectionChangedCount = 0; TextSelection? updatedSelection; - final TextSelectionDelegate delegate = FakeEditableTextState(); - const TextSpan text = TextSpan( - text: 'abc def ghi', + const String text = 'abc def ghi'; + final TextSelectionDelegate delegate = FakeEditableTextState() + ..textEditingValue = const TextEditingValue(text: text); + const TextSpan span = TextSpan( + text: text, style: TextStyle( height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', ), @@ -553,7 +559,7 @@ void main() { }, startHandleLayerLink: LayerLink(), endHandleLayerLink: LayerLink(), - text: text, + text: span, ); layout(editable1); @@ -574,7 +580,7 @@ void main() { selectionChangedCount++; updatedSelection = selection; }, - text: text, + text: span, startHandleLayerLink: LayerLink(), endHandleLayerLink: LayerLink(), ); @@ -933,8 +939,10 @@ void main() { expect(delegate.textEditingValue.text, '01232345'); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021 - test('arrow keys and delete handle surrogate pairs correctly', () async { - final TextSelectionDelegate delegate = FakeEditableTextState(); + test('arrow keys and delete handle surrogate pairs correctly case 2', () async { + const String text = '\u{1F44D}'; + final TextSelectionDelegate delegate = FakeEditableTextState() + ..textEditingValue = const TextEditingValue(text: text); final ViewportOffset viewportOffset = ViewportOffset.zero(); late TextSelection currentSelection; final RenderEditable editable = RenderEditable( @@ -951,7 +959,7 @@ void main() { startHandleLayerLink: LayerLink(), endHandleLayerLink: LayerLink(), text: const TextSpan( - text: '\u{1F44D}', // Thumbs up + text: text, // Thumbs up style: TextStyle( height: 1.0, fontSize: 10.0, fontFamily: 'Ahem', ), @@ -1038,7 +1046,9 @@ void main() { }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021 test('arrow keys with selection text', () async { - final TextSelectionDelegate delegate = FakeEditableTextState(); + const String text = '012345'; + final TextSelectionDelegate delegate = FakeEditableTextState() + ..textEditingValue = const TextEditingValue(text: text); final ViewportOffset viewportOffset = ViewportOffset.zero(); late TextSelection currentSelection; final RenderEditable editable = RenderEditable( @@ -1055,7 +1065,7 @@ void main() { startHandleLayerLink: LayerLink(), endHandleLayerLink: LayerLink(), text: const TextSpan( - text: '012345', // Thumbs up + text: text, // Thumbs up style: TextStyle(height: 1.0, fontSize: 10.0, fontFamily: 'Ahem'), ), selection: const TextSelection.collapsed( @@ -1096,7 +1106,9 @@ void main() { }, skip: isBrowser); // https://github.com/flutter/flutter/issues/58068 test('arrow keys with selection text and shift', () async { - final TextSelectionDelegate delegate = FakeEditableTextState(); + const String text = '012345'; + final TextSelectionDelegate delegate = FakeEditableTextState() + ..textEditingValue = const TextEditingValue(text: text); final ViewportOffset viewportOffset = ViewportOffset.zero(); late TextSelection currentSelection; final RenderEditable editable = RenderEditable( @@ -1113,7 +1125,7 @@ void main() { startHandleLayerLink: LayerLink(), endHandleLayerLink: LayerLink(), text: const TextSpan( - text: '012345', // Thumbs up + text: text, // Thumbs up style: TextStyle(height: 1.0, fontSize: 10.0, fontFamily: 'Ahem'), ), selection: const TextSelection.collapsed( @@ -1158,7 +1170,9 @@ void main() { }, skip: isBrowser); // https://github.com/flutter/flutter/issues/58068 test('respects enableInteractiveSelection', () async { - final TextSelectionDelegate delegate = FakeEditableTextState(); + const String text = '012345'; + final TextSelectionDelegate delegate = FakeEditableTextState() + ..textEditingValue = const TextEditingValue(text: text); final ViewportOffset viewportOffset = ViewportOffset.zero(); late TextSelection currentSelection; final RenderEditable editable = RenderEditable( @@ -1175,7 +1189,7 @@ void main() { startHandleLayerLink: LayerLink(), endHandleLayerLink: LayerLink(), text: const TextSpan( - text: '012345', // Thumbs up + text: text, // Thumbs up style: TextStyle(height: 1.0, fontSize: 10.0, fontFamily: 'Ahem'), ), selection: const TextSelection.collapsed(