Skip to content

Commit

Permalink
Move TextRange from the framework to dart:ui. (flutter#13747)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
gspencergoog authored Nov 8, 2019
1 parent 9620273 commit f7e73b6
Show file tree
Hide file tree
Showing 4 changed files with 283 additions and 2 deletions.
91 changes: 89 additions & 2 deletions lib/ui/text.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1406,6 +1406,93 @@ class TextPosition {
}
}

/// A range of characters in a string of text.
class TextRange {
/// Creates a text range.
///
/// The [start] and [end] arguments must not be null. Both the [start] and
/// [end] must either be greater than or equal to zero or both exactly -1.
///
/// The text included in the range includes the character at [start], but not
/// the one at [end].
///
/// Instead of creating an empty text range, consider using the [empty]
/// constant.
const TextRange({
this.start,
this.end,
}) : assert(start != null && start >= -1),
assert(end != null && end >= -1);

/// A text range that starts and ends at offset.
///
/// The [offset] argument must be non-null and greater than or equal to -1.
const TextRange.collapsed(int offset)
: assert(offset != null && offset >= -1),
start = offset,
end = offset;

/// A text range that contains nothing and is not in the text.
static const TextRange empty = TextRange(start: -1, end: -1);

/// The index of the first character in the range.
///
/// If [start] and [end] are both -1, the text range is empty.
final int start;

/// The next index after the characters in this range.
///
/// If [start] and [end] are both -1, the text range is empty.
final int end;

/// Whether this range represents a valid position in the text.
bool get isValid => start >= 0 && end >= 0;

/// Whether this range is empty (but still potentially placed inside the text).
bool get isCollapsed => start == end;

/// Whether the start of this range precedes the end.
bool get isNormalized => end >= start;

/// The text before this range.
String textBefore(String text) {
assert(isNormalized);
return text.substring(0, start);
}

/// The text after this range.
String textAfter(String text) {
assert(isNormalized);
return text.substring(end);
}

/// The text inside this range.
String textInside(String text) {
assert(isNormalized);
return text.substring(start, end);
}

@override
bool operator ==(dynamic other) {
if (identical(this, other))
return true;
if (other is! TextRange)
return false;
final TextRange typedOther = other;
return typedOther.start == start
&& typedOther.end == end;
}

@override
int get hashCode => hashValues(
start.hashCode,
end.hashCode,
);

@override
String toString() => 'TextRange(start: $start, end: $end)';
}

/// Layout constraints for [Paragraph] objects.
///
/// Instances of this class are typically used with [Paragraph.layout].
Expand Down Expand Up @@ -1512,8 +1599,8 @@ enum BoxHeightStyle {
/// Defines various ways to horizontally bound the boxes returned by
/// [Paragraph.getBoxesForRange].
enum BoxWidthStyle {
// Provide tight bounding boxes that fit widths to the runs of each line
// independently.
/// Provide tight bounding boxes that fit widths to the runs of each line
/// independently.
tight,

/// Adds up to two additional boxes as needed at the beginning and/or end
Expand Down
84 changes: 84 additions & 0 deletions lib/web_ui/lib/src/ui/text.dart
Original file line number Diff line number Diff line change
Expand Up @@ -906,6 +906,90 @@ class TextPosition {
}
}

/// A range of characters in a string of text.
class TextRange {
/// Creates a text range.
///
/// The [start] and [end] arguments must not be null. Both the [start] and
/// [end] must either be greater than or equal to zero or both exactly -1.
///
/// Instead of creating an empty text range, consider using the [empty]
/// constant.
const TextRange({
this.start,
this.end,
}) : assert(start != null && start >= -1),
assert(end != null && end >= -1);

/// A text range that starts and ends at offset.
///
/// The [offset] argument must be non-null and greater than or equal to -1.
const TextRange.collapsed(int offset)
: assert(offset != null && offset >= -1),
start = offset,
end = offset;

/// A text range that contains nothing and is not in the text.
static const TextRange empty = TextRange(start: -1, end: -1);

/// The index of the first character in the range.
///
/// If [start] and [end] are both -1, the text range is empty.
final int start;

/// The next index after the characters in this range.
///
/// If [start] and [end] are both -1, the text range is empty.
final int end;

/// Whether this range represents a valid position in the text.
bool get isValid => start >= 0 && end >= 0;

/// Whether this range is empty (but still potentially placed inside the text).
bool get isCollapsed => start == end;

/// Whether the start of this range precedes the end.
bool get isNormalized => end >= start;

/// The text before this range.
String textBefore(String text) {
assert(isNormalized);
return text.substring(0, start);
}

/// The text after this range.
String textAfter(String text) {
assert(isNormalized);
return text.substring(end);
}

/// The text inside this range.
String textInside(String text) {
assert(isNormalized);
return text.substring(start, end);
}

@override
bool operator ==(dynamic other) {
if (identical(this, other))
return true;
if (other is! TextRange)
return false;
final TextRange typedOther = other;
return typedOther.start == start
&& typedOther.end == end;
}

@override
int get hashCode => hashValues(
start.hashCode,
end.hashCode,
);

@override
String toString() => 'TextRange(start: $start, end: $end)';
}

/// Layout constraints for [Paragraph] objects.
///
/// Instances of this class are typically used with [Paragraph.layout].
Expand Down
55 changes: 55 additions & 0 deletions lib/web_ui/test/text_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -262,4 +262,59 @@ void main() async {

debugEmulateFlutterTesterEnvironment = true;
});
group('TextRange', () {
test('empty ranges are correct', () {
const TextRange range = TextRange(start: -1, end: -1);
expect(range, equals(const TextRange.collapsed(-1)));
expect(range, equals(TextRange.empty));
});
test('isValid works', () {
expect(TextRange.empty.isValid, isFalse);
expect(const TextRange(start: 0, end: 0).isValid, isTrue);
expect(const TextRange(start: 0, end: 10).isValid, isTrue);
expect(const TextRange(start: 10, end: 10).isValid, isTrue);
expect(const TextRange(start: -1, end: 10).isValid, isFalse);
expect(const TextRange(start: 10, end: 0).isValid, isTrue);
expect(const TextRange(start: 10, end: -1).isValid, isFalse);
});
test('isCollapsed works', () {
expect(TextRange.empty.isCollapsed, isTrue);
expect(const TextRange(start: 0, end: 0).isCollapsed, isTrue);
expect(const TextRange(start: 0, end: 10).isCollapsed, isFalse);
expect(const TextRange(start: 10, end: 10).isCollapsed, isTrue);
expect(const TextRange(start: -1, end: 10).isCollapsed, isFalse);
expect(const TextRange(start: 10, end: 0).isCollapsed, isFalse);
expect(const TextRange(start: 10, end: -1).isCollapsed, isFalse);
});
test('isNormalized works', () {
expect(TextRange.empty.isNormalized, isTrue);
expect(const TextRange(start: 0, end: 0).isNormalized, isTrue);
expect(const TextRange(start: 0, end: 10).isNormalized, isTrue);
expect(const TextRange(start: 10, end: 10).isNormalized, isTrue);
expect(const TextRange(start: -1, end: 10).isNormalized, isTrue);
expect(const TextRange(start: 10, end: 0).isNormalized, isFalse);
expect(const TextRange(start: 10, end: -1).isNormalized, isFalse);
});
test('textBefore works', () {
expect(const TextRange(start: 0, end: 0).textBefore('hello'), isEmpty);
expect(const TextRange(start: 1, end: 1).textBefore('hello'), equals('h'));
expect(const TextRange(start: 1, end: 2).textBefore('hello'), equals('h'));
expect(const TextRange(start: 5, end: 5).textBefore('hello'), equals('hello'));
expect(const TextRange(start: 0, end: 5).textBefore('hello'), isEmpty);
});
test('textAfter works', () {
expect(const TextRange(start: 0, end: 0).textAfter('hello'), equals('hello'));
expect(const TextRange(start: 1, end: 1).textAfter('hello'), equals('ello'));
expect(const TextRange(start: 1, end: 2).textAfter('hello'), equals('llo'));
expect(const TextRange(start: 5, end: 5).textAfter('hello'), isEmpty);
expect(const TextRange(start: 0, end: 5).textAfter('hello'), isEmpty);
});
test('textInside works', () {
expect(const TextRange(start: 0, end: 0).textInside('hello'), isEmpty);
expect(const TextRange(start: 1, end: 1).textInside('hello'), isEmpty);
expect(const TextRange(start: 1, end: 2).textInside('hello'), equals('e'));
expect(const TextRange(start: 5, end: 5).textInside('hello'), isEmpty);
expect(const TextRange(start: 0, end: 5).textInside('hello'), equals('hello'));
});
});
}
55 changes: 55 additions & 0 deletions testing/dart/text_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,59 @@ void main() {
expect(FontWeight.lerp(FontWeight.w400, null, 1), equals(FontWeight.w400));
});
});
group('TextRange', () {
test('empty ranges are correct', () {
const TextRange range = TextRange(start: -1, end: -1);
expect(range, equals(const TextRange.collapsed(-1)));
expect(range, equals(TextRange.empty));
});
test('isValid works', () {
expect(TextRange.empty.isValid, isFalse);
expect(const TextRange(start: 0, end: 0).isValid, isTrue);
expect(const TextRange(start: 0, end: 10).isValid, isTrue);
expect(const TextRange(start: 10, end: 10).isValid, isTrue);
expect(const TextRange(start: -1, end: 10).isValid, isFalse);
expect(const TextRange(start: 10, end: 0).isValid, isTrue);
expect(const TextRange(start: 10, end: -1).isValid, isFalse);
});
test('isCollapsed works', () {
expect(TextRange.empty.isCollapsed, isTrue);
expect(const TextRange(start: 0, end: 0).isCollapsed, isTrue);
expect(const TextRange(start: 0, end: 10).isCollapsed, isFalse);
expect(const TextRange(start: 10, end: 10).isCollapsed, isTrue);
expect(const TextRange(start: -1, end: 10).isCollapsed, isFalse);
expect(const TextRange(start: 10, end: 0).isCollapsed, isFalse);
expect(const TextRange(start: 10, end: -1).isCollapsed, isFalse);
});
test('isNormalized works', () {
expect(TextRange.empty.isNormalized, isTrue);
expect(const TextRange(start: 0, end: 0).isNormalized, isTrue);
expect(const TextRange(start: 0, end: 10).isNormalized, isTrue);
expect(const TextRange(start: 10, end: 10).isNormalized, isTrue);
expect(const TextRange(start: -1, end: 10).isNormalized, isTrue);
expect(const TextRange(start: 10, end: 0).isNormalized, isFalse);
expect(const TextRange(start: 10, end: -1).isNormalized, isFalse);
});
test('textBefore works', () {
expect(const TextRange(start: 0, end: 0).textBefore('hello'), isEmpty);
expect(const TextRange(start: 1, end: 1).textBefore('hello'), equals('h'));
expect(const TextRange(start: 1, end: 2).textBefore('hello'), equals('h'));
expect(const TextRange(start: 5, end: 5).textBefore('hello'), equals('hello'));
expect(const TextRange(start: 0, end: 5).textBefore('hello'), isEmpty);
});
test('textAfter works', () {
expect(const TextRange(start: 0, end: 0).textAfter('hello'), equals('hello'));
expect(const TextRange(start: 1, end: 1).textAfter('hello'), equals('ello'));
expect(const TextRange(start: 1, end: 2).textAfter('hello'), equals('llo'));
expect(const TextRange(start: 5, end: 5).textAfter('hello'), isEmpty);
expect(const TextRange(start: 0, end: 5).textAfter('hello'), isEmpty);
});
test('textInside works', () {
expect(const TextRange(start: 0, end: 0).textInside('hello'), isEmpty);
expect(const TextRange(start: 1, end: 1).textInside('hello'), isEmpty);
expect(const TextRange(start: 1, end: 2).textInside('hello'), equals('e'));
expect(const TextRange(start: 5, end: 5).textInside('hello'), isEmpty);
expect(const TextRange(start: 0, end: 5).textInside('hello'), equals('hello'));
});
});
}

0 comments on commit f7e73b6

Please sign in to comment.