Skip to content

Commit f7e73b6

Browse files
authored
Move TextRange from the framework to dart:ui. (flutter#13747)
This removes TextRange from the framework and moves it to the engine, in preparation for using it to return text ranges from the text extent APIs, like Paragraph.getWordBoundary instead of a List<int>. Also added new tests for TextRange.
1 parent 9620273 commit f7e73b6

File tree

4 files changed

+283
-2
lines changed

4 files changed

+283
-2
lines changed

lib/ui/text.dart

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1406,6 +1406,93 @@ class TextPosition {
14061406
}
14071407
}
14081408

1409+
/// A range of characters in a string of text.
1410+
class TextRange {
1411+
/// Creates a text range.
1412+
///
1413+
/// The [start] and [end] arguments must not be null. Both the [start] and
1414+
/// [end] must either be greater than or equal to zero or both exactly -1.
1415+
///
1416+
/// The text included in the range includes the character at [start], but not
1417+
/// the one at [end].
1418+
///
1419+
/// Instead of creating an empty text range, consider using the [empty]
1420+
/// constant.
1421+
const TextRange({
1422+
this.start,
1423+
this.end,
1424+
}) : assert(start != null && start >= -1),
1425+
assert(end != null && end >= -1);
1426+
1427+
/// A text range that starts and ends at offset.
1428+
///
1429+
/// The [offset] argument must be non-null and greater than or equal to -1.
1430+
const TextRange.collapsed(int offset)
1431+
: assert(offset != null && offset >= -1),
1432+
start = offset,
1433+
end = offset;
1434+
1435+
/// A text range that contains nothing and is not in the text.
1436+
static const TextRange empty = TextRange(start: -1, end: -1);
1437+
1438+
/// The index of the first character in the range.
1439+
///
1440+
/// If [start] and [end] are both -1, the text range is empty.
1441+
final int start;
1442+
1443+
/// The next index after the characters in this range.
1444+
///
1445+
/// If [start] and [end] are both -1, the text range is empty.
1446+
final int end;
1447+
1448+
/// Whether this range represents a valid position in the text.
1449+
bool get isValid => start >= 0 && end >= 0;
1450+
1451+
/// Whether this range is empty (but still potentially placed inside the text).
1452+
bool get isCollapsed => start == end;
1453+
1454+
/// Whether the start of this range precedes the end.
1455+
bool get isNormalized => end >= start;
1456+
1457+
/// The text before this range.
1458+
String textBefore(String text) {
1459+
assert(isNormalized);
1460+
return text.substring(0, start);
1461+
}
1462+
1463+
/// The text after this range.
1464+
String textAfter(String text) {
1465+
assert(isNormalized);
1466+
return text.substring(end);
1467+
}
1468+
1469+
/// The text inside this range.
1470+
String textInside(String text) {
1471+
assert(isNormalized);
1472+
return text.substring(start, end);
1473+
}
1474+
1475+
@override
1476+
bool operator ==(dynamic other) {
1477+
if (identical(this, other))
1478+
return true;
1479+
if (other is! TextRange)
1480+
return false;
1481+
final TextRange typedOther = other;
1482+
return typedOther.start == start
1483+
&& typedOther.end == end;
1484+
}
1485+
1486+
@override
1487+
int get hashCode => hashValues(
1488+
start.hashCode,
1489+
end.hashCode,
1490+
);
1491+
1492+
@override
1493+
String toString() => 'TextRange(start: $start, end: $end)';
1494+
}
1495+
14091496
/// Layout constraints for [Paragraph] objects.
14101497
///
14111498
/// Instances of this class are typically used with [Paragraph.layout].
@@ -1512,8 +1599,8 @@ enum BoxHeightStyle {
15121599
/// Defines various ways to horizontally bound the boxes returned by
15131600
/// [Paragraph.getBoxesForRange].
15141601
enum BoxWidthStyle {
1515-
// Provide tight bounding boxes that fit widths to the runs of each line
1516-
// independently.
1602+
/// Provide tight bounding boxes that fit widths to the runs of each line
1603+
/// independently.
15171604
tight,
15181605

15191606
/// Adds up to two additional boxes as needed at the beginning and/or end

lib/web_ui/lib/src/ui/text.dart

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -906,6 +906,90 @@ class TextPosition {
906906
}
907907
}
908908

909+
/// A range of characters in a string of text.
910+
class TextRange {
911+
/// Creates a text range.
912+
///
913+
/// The [start] and [end] arguments must not be null. Both the [start] and
914+
/// [end] must either be greater than or equal to zero or both exactly -1.
915+
///
916+
/// Instead of creating an empty text range, consider using the [empty]
917+
/// constant.
918+
const TextRange({
919+
this.start,
920+
this.end,
921+
}) : assert(start != null && start >= -1),
922+
assert(end != null && end >= -1);
923+
924+
/// A text range that starts and ends at offset.
925+
///
926+
/// The [offset] argument must be non-null and greater than or equal to -1.
927+
const TextRange.collapsed(int offset)
928+
: assert(offset != null && offset >= -1),
929+
start = offset,
930+
end = offset;
931+
932+
/// A text range that contains nothing and is not in the text.
933+
static const TextRange empty = TextRange(start: -1, end: -1);
934+
935+
/// The index of the first character in the range.
936+
///
937+
/// If [start] and [end] are both -1, the text range is empty.
938+
final int start;
939+
940+
/// The next index after the characters in this range.
941+
///
942+
/// If [start] and [end] are both -1, the text range is empty.
943+
final int end;
944+
945+
/// Whether this range represents a valid position in the text.
946+
bool get isValid => start >= 0 && end >= 0;
947+
948+
/// Whether this range is empty (but still potentially placed inside the text).
949+
bool get isCollapsed => start == end;
950+
951+
/// Whether the start of this range precedes the end.
952+
bool get isNormalized => end >= start;
953+
954+
/// The text before this range.
955+
String textBefore(String text) {
956+
assert(isNormalized);
957+
return text.substring(0, start);
958+
}
959+
960+
/// The text after this range.
961+
String textAfter(String text) {
962+
assert(isNormalized);
963+
return text.substring(end);
964+
}
965+
966+
/// The text inside this range.
967+
String textInside(String text) {
968+
assert(isNormalized);
969+
return text.substring(start, end);
970+
}
971+
972+
@override
973+
bool operator ==(dynamic other) {
974+
if (identical(this, other))
975+
return true;
976+
if (other is! TextRange)
977+
return false;
978+
final TextRange typedOther = other;
979+
return typedOther.start == start
980+
&& typedOther.end == end;
981+
}
982+
983+
@override
984+
int get hashCode => hashValues(
985+
start.hashCode,
986+
end.hashCode,
987+
);
988+
989+
@override
990+
String toString() => 'TextRange(start: $start, end: $end)';
991+
}
992+
909993
/// Layout constraints for [Paragraph] objects.
910994
///
911995
/// Instances of this class are typically used with [Paragraph.layout].

lib/web_ui/test/text_test.dart

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,4 +262,59 @@ void main() async {
262262

263263
debugEmulateFlutterTesterEnvironment = true;
264264
});
265+
group('TextRange', () {
266+
test('empty ranges are correct', () {
267+
const TextRange range = TextRange(start: -1, end: -1);
268+
expect(range, equals(const TextRange.collapsed(-1)));
269+
expect(range, equals(TextRange.empty));
270+
});
271+
test('isValid works', () {
272+
expect(TextRange.empty.isValid, isFalse);
273+
expect(const TextRange(start: 0, end: 0).isValid, isTrue);
274+
expect(const TextRange(start: 0, end: 10).isValid, isTrue);
275+
expect(const TextRange(start: 10, end: 10).isValid, isTrue);
276+
expect(const TextRange(start: -1, end: 10).isValid, isFalse);
277+
expect(const TextRange(start: 10, end: 0).isValid, isTrue);
278+
expect(const TextRange(start: 10, end: -1).isValid, isFalse);
279+
});
280+
test('isCollapsed works', () {
281+
expect(TextRange.empty.isCollapsed, isTrue);
282+
expect(const TextRange(start: 0, end: 0).isCollapsed, isTrue);
283+
expect(const TextRange(start: 0, end: 10).isCollapsed, isFalse);
284+
expect(const TextRange(start: 10, end: 10).isCollapsed, isTrue);
285+
expect(const TextRange(start: -1, end: 10).isCollapsed, isFalse);
286+
expect(const TextRange(start: 10, end: 0).isCollapsed, isFalse);
287+
expect(const TextRange(start: 10, end: -1).isCollapsed, isFalse);
288+
});
289+
test('isNormalized works', () {
290+
expect(TextRange.empty.isNormalized, isTrue);
291+
expect(const TextRange(start: 0, end: 0).isNormalized, isTrue);
292+
expect(const TextRange(start: 0, end: 10).isNormalized, isTrue);
293+
expect(const TextRange(start: 10, end: 10).isNormalized, isTrue);
294+
expect(const TextRange(start: -1, end: 10).isNormalized, isTrue);
295+
expect(const TextRange(start: 10, end: 0).isNormalized, isFalse);
296+
expect(const TextRange(start: 10, end: -1).isNormalized, isFalse);
297+
});
298+
test('textBefore works', () {
299+
expect(const TextRange(start: 0, end: 0).textBefore('hello'), isEmpty);
300+
expect(const TextRange(start: 1, end: 1).textBefore('hello'), equals('h'));
301+
expect(const TextRange(start: 1, end: 2).textBefore('hello'), equals('h'));
302+
expect(const TextRange(start: 5, end: 5).textBefore('hello'), equals('hello'));
303+
expect(const TextRange(start: 0, end: 5).textBefore('hello'), isEmpty);
304+
});
305+
test('textAfter works', () {
306+
expect(const TextRange(start: 0, end: 0).textAfter('hello'), equals('hello'));
307+
expect(const TextRange(start: 1, end: 1).textAfter('hello'), equals('ello'));
308+
expect(const TextRange(start: 1, end: 2).textAfter('hello'), equals('llo'));
309+
expect(const TextRange(start: 5, end: 5).textAfter('hello'), isEmpty);
310+
expect(const TextRange(start: 0, end: 5).textAfter('hello'), isEmpty);
311+
});
312+
test('textInside works', () {
313+
expect(const TextRange(start: 0, end: 0).textInside('hello'), isEmpty);
314+
expect(const TextRange(start: 1, end: 1).textInside('hello'), isEmpty);
315+
expect(const TextRange(start: 1, end: 2).textInside('hello'), equals('e'));
316+
expect(const TextRange(start: 5, end: 5).textInside('hello'), isEmpty);
317+
expect(const TextRange(start: 0, end: 5).textInside('hello'), equals('hello'));
318+
});
319+
});
265320
}

testing/dart/text_test.dart

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,59 @@ void main() {
2424
expect(FontWeight.lerp(FontWeight.w400, null, 1), equals(FontWeight.w400));
2525
});
2626
});
27+
group('TextRange', () {
28+
test('empty ranges are correct', () {
29+
const TextRange range = TextRange(start: -1, end: -1);
30+
expect(range, equals(const TextRange.collapsed(-1)));
31+
expect(range, equals(TextRange.empty));
32+
});
33+
test('isValid works', () {
34+
expect(TextRange.empty.isValid, isFalse);
35+
expect(const TextRange(start: 0, end: 0).isValid, isTrue);
36+
expect(const TextRange(start: 0, end: 10).isValid, isTrue);
37+
expect(const TextRange(start: 10, end: 10).isValid, isTrue);
38+
expect(const TextRange(start: -1, end: 10).isValid, isFalse);
39+
expect(const TextRange(start: 10, end: 0).isValid, isTrue);
40+
expect(const TextRange(start: 10, end: -1).isValid, isFalse);
41+
});
42+
test('isCollapsed works', () {
43+
expect(TextRange.empty.isCollapsed, isTrue);
44+
expect(const TextRange(start: 0, end: 0).isCollapsed, isTrue);
45+
expect(const TextRange(start: 0, end: 10).isCollapsed, isFalse);
46+
expect(const TextRange(start: 10, end: 10).isCollapsed, isTrue);
47+
expect(const TextRange(start: -1, end: 10).isCollapsed, isFalse);
48+
expect(const TextRange(start: 10, end: 0).isCollapsed, isFalse);
49+
expect(const TextRange(start: 10, end: -1).isCollapsed, isFalse);
50+
});
51+
test('isNormalized works', () {
52+
expect(TextRange.empty.isNormalized, isTrue);
53+
expect(const TextRange(start: 0, end: 0).isNormalized, isTrue);
54+
expect(const TextRange(start: 0, end: 10).isNormalized, isTrue);
55+
expect(const TextRange(start: 10, end: 10).isNormalized, isTrue);
56+
expect(const TextRange(start: -1, end: 10).isNormalized, isTrue);
57+
expect(const TextRange(start: 10, end: 0).isNormalized, isFalse);
58+
expect(const TextRange(start: 10, end: -1).isNormalized, isFalse);
59+
});
60+
test('textBefore works', () {
61+
expect(const TextRange(start: 0, end: 0).textBefore('hello'), isEmpty);
62+
expect(const TextRange(start: 1, end: 1).textBefore('hello'), equals('h'));
63+
expect(const TextRange(start: 1, end: 2).textBefore('hello'), equals('h'));
64+
expect(const TextRange(start: 5, end: 5).textBefore('hello'), equals('hello'));
65+
expect(const TextRange(start: 0, end: 5).textBefore('hello'), isEmpty);
66+
});
67+
test('textAfter works', () {
68+
expect(const TextRange(start: 0, end: 0).textAfter('hello'), equals('hello'));
69+
expect(const TextRange(start: 1, end: 1).textAfter('hello'), equals('ello'));
70+
expect(const TextRange(start: 1, end: 2).textAfter('hello'), equals('llo'));
71+
expect(const TextRange(start: 5, end: 5).textAfter('hello'), isEmpty);
72+
expect(const TextRange(start: 0, end: 5).textAfter('hello'), isEmpty);
73+
});
74+
test('textInside works', () {
75+
expect(const TextRange(start: 0, end: 0).textInside('hello'), isEmpty);
76+
expect(const TextRange(start: 1, end: 1).textInside('hello'), isEmpty);
77+
expect(const TextRange(start: 1, end: 2).textInside('hello'), equals('e'));
78+
expect(const TextRange(start: 5, end: 5).textInside('hello'), isEmpty);
79+
expect(const TextRange(start: 0, end: 5).textInside('hello'), equals('hello'));
80+
});
81+
});
2782
}

0 commit comments

Comments
 (0)