@@ -11701,6 +11701,163 @@ void main() {
1170111701 expect (tester.hasRunningAnimations, isFalse);
1170211702 });
1170311703
11704+ testWidgets ('Floating cursor affinity' , (WidgetTester tester) async {
11705+ EditableText .debugDeterministicCursor = true ;
11706+ final FocusNode focusNode = FocusNode ();
11707+ final GlobalKey key = GlobalKey ();
11708+ // Set it up so that there will be word-wrap.
11709+ final TextEditingController controller = TextEditingController (text: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz' );
11710+ await tester.pumpWidget (
11711+ MaterialApp (
11712+ home: Center (
11713+ child: ConstrainedBox (
11714+ constraints: const BoxConstraints (
11715+ maxWidth: 500 ,
11716+ ),
11717+ child: EditableText (
11718+ key: key,
11719+ autofocus: true ,
11720+ maxLines: 2 ,
11721+ controller: controller,
11722+ focusNode: focusNode,
11723+ style: textStyle,
11724+ cursorColor: Colors .blue,
11725+ backgroundCursorColor: Colors .grey,
11726+ cursorOpacityAnimates: true ,
11727+ ),
11728+ ),
11729+ ),
11730+ ),
11731+ );
11732+
11733+ await tester.pump ();
11734+ final EditableTextState state = tester.state (find.byType (EditableText ));
11735+
11736+ // Select after the first word, with default affinity (downstream).
11737+ controller.selection = const TextSelection .collapsed (offset: 27 );
11738+ await tester.pump ();
11739+ state.updateFloatingCursor (RawFloatingCursorPoint (state: FloatingCursorDragState .Start , offset: Offset .zero));
11740+ await tester.pump ();
11741+
11742+ // The floating cursor should be drawn at the end of the first line.
11743+ expect (key.currentContext! .findRenderObject (), paints..rrect (
11744+ rrect: RRect .fromRectAndRadius (
11745+ const Rect .fromLTWH (0.5 , 15 , 3 , 12 ),
11746+ const Radius .circular (1 )
11747+ )
11748+ ));
11749+
11750+ // Select after the first word, with upstream affinity.
11751+ controller.selection = const TextSelection .collapsed (offset: 27 , affinity: TextAffinity .upstream);
11752+ await tester.pump ();
11753+
11754+ state.updateFloatingCursor (RawFloatingCursorPoint (state: FloatingCursorDragState .Start , offset: Offset .zero));
11755+ await tester.pump ();
11756+
11757+ // The floating cursor should be drawn at the beginning of the second line.
11758+ expect (key.currentContext! .findRenderObject (), paints..rrect (
11759+ rrect: RRect .fromRectAndRadius (
11760+ const Rect .fromLTWH (378.5 , 1 , 3 , 12 ),
11761+ const Radius .circular (1 )
11762+ )
11763+ ));
11764+
11765+ EditableText .debugDeterministicCursor = false ;
11766+ });
11767+
11768+ testWidgets ('Floating cursor ending with selection' , (WidgetTester tester) async {
11769+ EditableText .debugDeterministicCursor = true ;
11770+ final FocusNode focusNode = FocusNode ();
11771+ final GlobalKey key = GlobalKey ();
11772+ // Set it up so that there will be word-wrap.
11773+ final TextEditingController controller = TextEditingController (text: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' );
11774+ controller.selection = const TextSelection .collapsed (offset: 0 );
11775+ await tester.pumpWidget (
11776+ MaterialApp (
11777+ home: EditableText (
11778+ key: key,
11779+ autofocus: true ,
11780+ controller: controller,
11781+ focusNode: focusNode,
11782+ style: textStyle,
11783+ cursorColor: Colors .blue,
11784+ backgroundCursorColor: Colors .grey,
11785+ cursorOpacityAnimates: true ,
11786+ ),
11787+ ),
11788+ );
11789+
11790+ await tester.pump ();
11791+ final EditableTextState state = tester.state (find.byType (EditableText ));
11792+
11793+ state.updateFloatingCursor (RawFloatingCursorPoint (state: FloatingCursorDragState .Start , offset: Offset .zero));
11794+ await tester.pump ();
11795+
11796+ // The floating cursor should be drawn at the start of the line.
11797+ expect (key.currentContext! .findRenderObject (), paints..rrect (
11798+ rrect: RRect .fromRectAndRadius (
11799+ const Rect .fromLTWH (0.5 , 1 , 3 , 12 ),
11800+ const Radius .circular (1 )
11801+ )
11802+ ));
11803+
11804+ state.updateFloatingCursor (RawFloatingCursorPoint (state: FloatingCursorDragState .Update , offset: const Offset (50 , 0 )));
11805+ await tester.pump ();
11806+
11807+ // The floating cursor should be drawn somewhere in the middle of the line
11808+ expect (key.currentContext! .findRenderObject (), paints..rrect (
11809+ rrect: RRect .fromRectAndRadius (
11810+ const Rect .fromLTWH (50.5 , 1 , 3 , 12 ),
11811+ const Radius .circular (1 )
11812+ )
11813+ ));
11814+
11815+ state.updateFloatingCursor (RawFloatingCursorPoint (state: FloatingCursorDragState .End , offset: Offset .zero));
11816+ await tester.pumpAndSettle (const Duration (milliseconds: 125 )); // Floating cursor has an end animation.
11817+
11818+ // Selection should be updated based on the floating cursor location.
11819+ expect (controller.selection.isCollapsed, true );
11820+ expect (controller.selection.baseOffset, 4 );
11821+
11822+ state.updateFloatingCursor (RawFloatingCursorPoint (state: FloatingCursorDragState .Start , offset: Offset .zero));
11823+ await tester.pump ();
11824+
11825+ // The floating cursor should be drawn near to the previous position.
11826+ // It's different because it's snapped to exactly between characters.
11827+ expect (key.currentContext! .findRenderObject (), paints..rrect (
11828+ rrect: RRect .fromRectAndRadius (
11829+ const Rect .fromLTWH (56.5 , 1 , 3 , 12 ),
11830+ const Radius .circular (1 )
11831+ )
11832+ ));
11833+
11834+ state.updateFloatingCursor (RawFloatingCursorPoint (state: FloatingCursorDragState .Update , offset: const Offset (- 56 , 0 )));
11835+ await tester.pump ();
11836+
11837+ // The floating cursor should be drawn at the start of the line.
11838+ expect (key.currentContext! .findRenderObject (), paints..rrect (
11839+ rrect: RRect .fromRectAndRadius (
11840+ const Rect .fromLTWH (0.5 , 1 , 3 , 12 ),
11841+ const Radius .circular (1 )
11842+ )
11843+ ));
11844+
11845+ // Simulate UIKit setting the selection using keyboard selection.
11846+ controller.selection = const TextSelection (baseOffset: 0 , extentOffset: 4 );
11847+ await tester.pump ();
11848+
11849+ state.updateFloatingCursor (RawFloatingCursorPoint (state: FloatingCursorDragState .End , offset: Offset .zero));
11850+ await tester.pump ();
11851+
11852+ // Selection should not be updated as the new position is within the selection range.
11853+ expect (controller.selection.isCollapsed, false );
11854+ expect (controller.selection.baseOffset, 0 );
11855+ expect (controller.selection.extentOffset, 4 );
11856+
11857+ EditableText .debugDeterministicCursor = false ;
11858+ });
11859+
11860+
1170411861 group ('Selection changed scroll into view' , () {
1170511862 final String text = List <int >.generate (64 , (int index) => index).join ('\n ' );
1170611863 final TextEditingController controller = TextEditingController (text: text);
0 commit comments