@@ -12,6 +12,10 @@ import 'package:flutter_test/flutter_test.dart';
1212
1313import 'rendering_tester.dart' ;
1414
15+ double _caretMarginOf (RenderEditable renderEditable) {
16+ return renderEditable.cursorWidth + 1.0 ;
17+ }
18+
1519void _applyParentData (List <RenderBox > inlineRenderBoxes, InlineSpan span) {
1620 int index = 0 ;
1721 RenderBox ? previousBox;
@@ -1184,8 +1188,107 @@ void main() {
11841188 });
11851189
11861190 group ('hit testing' , () {
1191+ final TextSelectionDelegate delegate = _FakeEditableTextState ();
1192+
1193+ test ('Basic TextSpan Hit testing' , () {
1194+ final TextSpan textSpanA = TextSpan (text: 'A' * 10 );
1195+ const TextSpan textSpanBC = TextSpan (text: 'BC' , style: TextStyle (letterSpacing: 26.0 ));
1196+
1197+ final TextSpan text = TextSpan (
1198+ text: '' ,
1199+ style: const TextStyle (fontSize: 10.0 ),
1200+ children: < InlineSpan > [textSpanA, textSpanBC],
1201+ );
1202+
1203+ final RenderEditable renderEditable = RenderEditable (
1204+ text: text,
1205+ maxLines: null ,
1206+ startHandleLayerLink: LayerLink (),
1207+ endHandleLayerLink: LayerLink (),
1208+ textDirection: TextDirection .ltr,
1209+ offset: ViewportOffset .fixed (0.0 ),
1210+ textSelectionDelegate: delegate,
1211+ selection: const TextSelection .collapsed (offset: 0 ),
1212+ );
1213+ layout (renderEditable, constraints: BoxConstraints .tightFor (width: 100.0 + _caretMarginOf (renderEditable)));
1214+
1215+ BoxHitTestResult result;
1216+
1217+ // Hit-testing the first line
1218+ // First A
1219+ expect (renderEditable.hitTest (result = BoxHitTestResult (), position: const Offset (5.0 , 5.0 )), isTrue);
1220+ expect (result.path.map ((HitTestEntry <HitTestTarget > entry) => entry.target).whereType <TextSpan >(), < TextSpan > [textSpanA]);
1221+ // The last A.
1222+ expect (renderEditable.hitTest (result = BoxHitTestResult (), position: const Offset (95.0 , 5.0 )), isTrue);
1223+ expect (result.path.map ((HitTestEntry <HitTestTarget > entry) => entry.target).whereType <TextSpan >(), < TextSpan > [textSpanA]);
1224+ // Far away from the line.
1225+ expect (renderEditable.hitTest (result = BoxHitTestResult (), position: const Offset (200.0 , 5.0 )), isFalse);
1226+ expect (result.path.map ((HitTestEntry <HitTestTarget > entry) => entry.target).whereType <TextSpan >(), < TextSpan > []);
1227+
1228+ // Hit-testing the second line
1229+ // Tapping on B (startX = letter-spacing / 2 = 13.0).
1230+ expect (renderEditable.hitTest (result = BoxHitTestResult (), position: const Offset (18.0 , 15.0 )), isTrue);
1231+ expect (result.path.map ((HitTestEntry <HitTestTarget > entry) => entry.target).whereType <TextSpan >(), < TextSpan > [textSpanBC]);
1232+
1233+ // Between B and C, with large letter-spacing.
1234+ expect (renderEditable.hitTest (result = BoxHitTestResult (), position: const Offset (31.0 , 15.0 )), isTrue);
1235+ expect (result.path.map ((HitTestEntry <HitTestTarget > entry) => entry.target).whereType <TextSpan >(), < TextSpan > [textSpanBC]);
1236+
1237+ // On C.
1238+ expect (renderEditable.hitTest (result = BoxHitTestResult (), position: const Offset (54.0 , 15.0 )), isTrue);
1239+ expect (result.path.map ((HitTestEntry <HitTestTarget > entry) => entry.target).whereType <TextSpan >(), < TextSpan > [textSpanBC]);
1240+
1241+ // After C.
1242+ expect (renderEditable.hitTest (result = BoxHitTestResult (), position: const Offset (100.0 , 15.0 )), isTrue);
1243+ expect (result.path.map ((HitTestEntry <HitTestTarget > entry) => entry.target).whereType <TextSpan >(), < TextSpan > []);
1244+
1245+ // Not even remotely close.
1246+ expect (renderEditable.hitTest (result = BoxHitTestResult (), position: const Offset (9999.0 , 9999.0 )), isFalse);
1247+ expect (result.path.map ((HitTestEntry <HitTestTarget > entry) => entry.target).whereType <TextSpan >(), < TextSpan > []);
1248+ });
1249+
1250+ test ('TextSpan Hit testing with text justification' , () {
1251+ const TextSpan textSpanA = TextSpan (text: 'A ' ); // The space is a word break.
1252+ const TextSpan textSpanB = TextSpan (text: 'B\u 200B' ); // The zero-width space is used as a line break.
1253+ final TextSpan textSpanC = TextSpan (text: 'C' * 10 ); // The third span starts a new line since it's too long for the first line.
1254+
1255+ // The text should look like:
1256+ // A B
1257+ // CCCCCCCCCC
1258+ final TextSpan text = TextSpan (
1259+ text: '' ,
1260+ style: const TextStyle (fontSize: 10.0 ),
1261+ children: < InlineSpan > [textSpanA, textSpanB, textSpanC],
1262+ );
1263+ final RenderEditable renderEditable = RenderEditable (
1264+ text: text,
1265+ maxLines: null ,
1266+ startHandleLayerLink: LayerLink (),
1267+ endHandleLayerLink: LayerLink (),
1268+ textDirection: TextDirection .ltr,
1269+ textAlign: TextAlign .justify,
1270+ offset: ViewportOffset .fixed (0.0 ),
1271+ textSelectionDelegate: delegate,
1272+ selection: const TextSelection .collapsed (offset: 0 ),
1273+ );
1274+
1275+ layout (renderEditable, constraints: BoxConstraints .tightFor (width: 100.0 + _caretMarginOf (renderEditable)));
1276+ BoxHitTestResult result;
1277+
1278+ // Tapping on A.
1279+ expect (renderEditable.hitTest (result = BoxHitTestResult (), position: const Offset (5.0 , 5.0 )), isTrue);
1280+ expect (result.path.map ((HitTestEntry <HitTestTarget > entry) => entry.target).whereType <TextSpan >(), < TextSpan > [textSpanA]);
1281+
1282+ // Between A and B.
1283+ expect (renderEditable.hitTest (result = BoxHitTestResult (), position: const Offset (50.0 , 5.0 )), isTrue);
1284+ expect (result.path.map ((HitTestEntry <HitTestTarget > entry) => entry.target).whereType <TextSpan >(), < TextSpan > [textSpanA]);
1285+
1286+ // On B.
1287+ expect (renderEditable.hitTest (result = BoxHitTestResult (), position: const Offset (95.0 , 5.0 )), isTrue);
1288+ expect (result.path.map ((HitTestEntry <HitTestTarget > entry) => entry.target).whereType <TextSpan >(), < TextSpan > [textSpanB]);
1289+ });
1290+
11871291 test ('hits correct TextSpan when not scrolled' , () {
1188- final TextSelectionDelegate delegate = _FakeEditableTextState ();
11891292 final RenderEditable editable = RenderEditable (
11901293 text: const TextSpan (
11911294 style: TextStyle (height: 1.0 , fontSize: 10.0 ),
@@ -1692,7 +1795,8 @@ void main() {
16921795 // Prepare for painting after layout.
16931796 pumpFrame (phase: EnginePhase .compositingBits);
16941797 BoxHitTestResult result = BoxHitTestResult ();
1695- editable.hitTest (result, position: Offset .zero);
1798+ // The WidgetSpans have a height of 14.0, so "test" has a y offset of 4.0.
1799+ editable.hitTest (result, position: const Offset (1.0 , 5.0 ));
16961800 // We expect two hit test entries in the path because the RenderEditable
16971801 // will add itself as well.
16981802 expect (result.path, hasLength (2 ));
@@ -1702,7 +1806,7 @@ void main() {
17021806 // Only testing the RenderEditable entry here once, not anymore below.
17031807 expect (result.path.last.target, isA <RenderEditable >());
17041808 result = BoxHitTestResult ();
1705- editable.hitTest (result, position: const Offset (15.0 , 0 .0 ));
1809+ editable.hitTest (result, position: const Offset (15.0 , 5 .0 ));
17061810 expect (result.path, hasLength (2 ));
17071811 target = result.path.first.target;
17081812 expect (target, isA <TextSpan >());
@@ -1775,7 +1879,8 @@ void main() {
17751879 // Prepare for painting after layout.
17761880 pumpFrame (phase: EnginePhase .compositingBits);
17771881 BoxHitTestResult result = BoxHitTestResult ();
1778- editable.hitTest (result, position: Offset .zero);
1882+ // The WidgetSpans have a height of 14.0, so "test" has a y offset of 4.0.
1883+ editable.hitTest (result, position: const Offset (0.0 , 4.0 ));
17791884 // We expect two hit test entries in the path because the RenderEditable
17801885 // will add itself as well.
17811886 expect (result.path, hasLength (2 ));
@@ -1785,13 +1890,14 @@ void main() {
17851890 // Only testing the RenderEditable entry here once, not anymore below.
17861891 expect (result.path.last.target, isA <RenderEditable >());
17871892 result = BoxHitTestResult ();
1788- editable.hitTest (result, position: const Offset (15.0 , 0 .0 ));
1893+ editable.hitTest (result, position: const Offset (15.0 , 4 .0 ));
17891894 expect (result.path, hasLength (2 ));
17901895 target = result.path.first.target;
17911896 expect (target, isA <TextSpan >());
17921897 expect ((target as TextSpan ).text, text);
17931898
17941899 result = BoxHitTestResult ();
1900+ // "test" is 40 pixel wide.
17951901 editable.hitTest (result, position: const Offset (41.0 , 0.0 ));
17961902 expect (result.path, hasLength (3 ));
17971903 target = result.path.first.target;
@@ -1814,7 +1920,7 @@ void main() {
18141920
18151921 result = BoxHitTestResult ();
18161922 editable.hitTest (result, position: const Offset (5.0 , 15.0 ));
1817- expect (result.path, hasLength (2 ));
1923+ expect (result.path, hasLength (1 )); // Only the RenderEditable.
18181924 }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61020
18191925 });
18201926
0 commit comments