Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1676,59 +1676,60 @@ - (CGRect)firstRectForRange:(UITextRange*)range {
}
}

NSUInteger first = start;
if (end < start) {
first = end;
}

CGRect startSelectionRect = CGRectNull;
CGRect endSelectionRect = CGRectNull;
// Selection rects from different langauges may have different minY/maxY.
// So we need to iterate through each rects to update minY/maxY.
CGFloat minY = CGFLOAT_MAX;
CGFloat maxY = CGFLOAT_MIN;
if (@available(iOS 17.0, *)) {
NSUInteger first = start;
if (end < start) {
first = end;
}

FlutterTextRange* textRange = [FlutterTextRange
rangeWithNSRange:fml::RangeForCharactersInRange(self.text, NSMakeRange(0, self.text.length))];
for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
BOOL startsOnOrBeforeStartOfRange = _selectionRects[i].position <= first;
BOOL isLastSelectionRect = i + 1 == [_selectionRects count];
BOOL endOfTextIsAfterStartOfRange = isLastSelectionRect && textRange.range.length > first;
BOOL nextSelectionRectIsAfterStartOfRange =
!isLastSelectionRect && _selectionRects[i + 1].position > first;
if (startsOnOrBeforeStartOfRange &&
(endOfTextIsAfterStartOfRange || nextSelectionRectIsAfterStartOfRange)) {
// TODO(hellohaunlin): Remove iOS 17 check. The logic should also work for older versions.
if (@available(iOS 17, *)) {
CGRect startSelectionRect = CGRectNull;
CGRect endSelectionRect = CGRectNull;
// Selection rects from different langauges may have different minY/maxY.
// So we need to iterate through each rects to update minY/maxY.
CGFloat minY = CGFLOAT_MAX;
CGFloat maxY = CGFLOAT_MIN;

FlutterTextRange* textRange =
[FlutterTextRange rangeWithNSRange:fml::RangeForCharactersInRange(
self.text, NSMakeRange(0, self.text.length))];
for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
BOOL startsOnOrBeforeStartOfRange = _selectionRects[i].position <= first;
BOOL isLastSelectionRect = i + 1 == [_selectionRects count];
BOOL endOfTextIsAfterStartOfRange = isLastSelectionRect && textRange.range.length > first;
BOOL nextSelectionRectIsAfterStartOfRange =
!isLastSelectionRect && _selectionRects[i + 1].position > first;
if (startsOnOrBeforeStartOfRange &&
(endOfTextIsAfterStartOfRange || nextSelectionRectIsAfterStartOfRange)) {
startSelectionRect = _selectionRects[i].rect;
} else {
return _selectionRects[i].rect;
}
}
if (!CGRectIsNull(startSelectionRect)) {
minY = fmin(minY, CGRectGetMinY(_selectionRects[i].rect));
maxY = fmax(maxY, CGRectGetMaxY(_selectionRects[i].rect));
BOOL endsOnOrAfterEndOfRange = _selectionRects[i].position >= end - 1; // end is exclusive
BOOL nextSelectionRectIsOnNextLine =
!isLastSelectionRect &&
// Selection rects from different langauges in 2 lines may overlap with each other.
// A good approximation is to check if the center of next rect is below the bottom of
// current rect.
// TODO(hellohuanlin): Consider passing the line break info from framework.
CGRectGetMidY(_selectionRects[i + 1].rect) > CGRectGetMaxY(_selectionRects[i].rect);
if (endsOnOrAfterEndOfRange || isLastSelectionRect || nextSelectionRectIsOnNextLine) {
endSelectionRect = _selectionRects[i].rect;
break;
if (!CGRectIsNull(startSelectionRect)) {
minY = fmin(minY, CGRectGetMinY(_selectionRects[i].rect));
maxY = fmax(maxY, CGRectGetMaxY(_selectionRects[i].rect));
BOOL endsOnOrAfterEndOfRange = _selectionRects[i].position >= end - 1; // end is exclusive
BOOL nextSelectionRectIsOnNextLine =
!isLastSelectionRect &&
// Selection rects from different langauges in 2 lines may overlap with each other.
// A good approximation is to check if the center of next rect is below the bottom of
// current rect.
// TODO(hellohuanlin): Consider passing the line break info from framework.
CGRectGetMidY(_selectionRects[i + 1].rect) > CGRectGetMaxY(_selectionRects[i].rect);
if (endsOnOrAfterEndOfRange || isLastSelectionRect || nextSelectionRectIsOnNextLine) {
endSelectionRect = _selectionRects[i].rect;
break;
}
}
}
}
if (CGRectIsNull(startSelectionRect) || CGRectIsNull(endSelectionRect)) {
return CGRectZero;
if (CGRectIsNull(startSelectionRect) || CGRectIsNull(endSelectionRect)) {
return CGRectZero;
} else {
// fmin/fmax to support both LTR and RTL languages.
CGFloat minX = fmin(CGRectGetMinX(startSelectionRect), CGRectGetMinX(endSelectionRect));
CGFloat maxX = fmax(CGRectGetMaxX(startSelectionRect), CGRectGetMaxX(endSelectionRect));
return CGRectMake(minX, minY, maxX - minX, maxY - minY);
}
} else {
// fmin/fmax to support both LTR and RTL languages.
CGFloat minX = fmin(CGRectGetMinX(startSelectionRect), CGRectGetMinX(endSelectionRect));
CGFloat maxX = fmax(CGRectGetMaxX(startSelectionRect), CGRectGetMaxX(endSelectionRect));
return CGRectMake(minX, minY, maxX - minX, maxY - minY);
// Auto correction highlight on iOS 16 and older is rendered by Flutter.
return CGRectZero;
Copy link
Contributor Author

@hellohuanlin hellohuanlin Oct 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The initial implementation of firstRectForRange that returns the first selection rect (which, by the way, was incorrect) was introduced for the scribble feature on iPad (code here). It turns out that the change was not necessary for scribble - I have verified that keep returning CGRectZero doesn't break scribble.

}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1569,17 +1569,20 @@ - (void)testFirstRectForRangeReturnsCorrectRectOnASingleLineLeftToRight {
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 0, 100, 100) position:3U],
]];
FlutterTextRange* singleRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 1)];
XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
[inputView firstRectForRange:singleRectRange]));
if (@available(iOS 17, *)) {
XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
[inputView firstRectForRange:singleRectRange]));
} else {
XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
}

FlutterTextRange* multiRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 3)];

if (@available(iOS 17, *)) {
XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
[inputView firstRectForRange:multiRectRange]));
} else {
XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
[inputView firstRectForRange:multiRectRange]));
XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
}

[inputView setTextInputState:@{@"text" : @"COM"}];
Expand All @@ -1598,16 +1601,19 @@ - (void)testFirstRectForRangeReturnsCorrectRectOnASingleLineRightToLeft {
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:3U],
]];
FlutterTextRange* singleRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 1)];
XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
[inputView firstRectForRange:singleRectRange]));
if (@available(iOS 17, *)) {
XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
[inputView firstRectForRange:singleRectRange]));
} else {
XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
}

FlutterTextRange* multiRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 3)];
if (@available(iOS 17, *)) {
XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
[inputView firstRectForRange:multiRectRange]));
} else {
XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
[inputView firstRectForRange:multiRectRange]));
XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
}

[inputView setTextInputState:@{@"text" : @"COM"}];
Expand All @@ -1630,17 +1636,20 @@ - (void)testFirstRectForRangeReturnsCorrectRectOnMultipleLinesLeftToRight {
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 100, 100, 100) position:7U],
]];
FlutterTextRange* singleRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 1)];
XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
[inputView firstRectForRange:singleRectRange]));
if (@available(iOS 17, *)) {
XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
[inputView firstRectForRange:singleRectRange]));
} else {
XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
}

FlutterTextRange* multiRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 4)];

if (@available(iOS 17, *)) {
XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
[inputView firstRectForRange:multiRectRange]));
} else {
XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
[inputView firstRectForRange:multiRectRange]));
XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
}
}

Expand All @@ -1659,16 +1668,19 @@ - (void)testFirstRectForRangeReturnsCorrectRectOnMultipleLinesRightToLeft {
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 100, 100, 100) position:7U],
]];
FlutterTextRange* singleRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 1)];
XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
[inputView firstRectForRange:singleRectRange]));
if (@available(iOS 17, *)) {
XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
[inputView firstRectForRange:singleRectRange]));
} else {
XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
}

FlutterTextRange* multiRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 4)];
if (@available(iOS 17, *)) {
XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
[inputView firstRectForRange:multiRectRange]));
} else {
XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
[inputView firstRectForRange:multiRectRange]));
XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
}
}

Expand All @@ -1691,8 +1703,7 @@ - (void)testFirstRectForRangeReturnsCorrectRectOnSingleLineWithVaryingMinYAndMax
XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, -10, 300, 120),
[inputView firstRectForRange:multiRectRange]));
} else {
XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 10, 100, 80),
[inputView firstRectForRange:multiRectRange]));
XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
}
}

Expand All @@ -1715,8 +1726,7 @@ - (void)testFirstRectForRangeReturnsCorrectRectOnSingleLineWithVaryingMinYAndMax
XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, -10, 300, 120),
[inputView firstRectForRange:multiRectRange]));
} else {
XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, -10, 100, 120),
[inputView firstRectForRange:multiRectRange]));
XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
}
}

Expand All @@ -1739,8 +1749,7 @@ - (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsExceedingThre
XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
[inputView firstRectForRange:multiRectRange]));
} else {
XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
[inputView firstRectForRange:multiRectRange]));
XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
}
}

Expand All @@ -1763,8 +1772,7 @@ - (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsExceedingThre
XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
[inputView firstRectForRange:multiRectRange]));
} else {
XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
[inputView firstRectForRange:multiRectRange]));
XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
}
}

Expand All @@ -1787,8 +1795,7 @@ - (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsWithinThresho
XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 400, 140),
[inputView firstRectForRange:multiRectRange]));
} else {
XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
[inputView firstRectForRange:multiRectRange]));
XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
}
}

Expand All @@ -1811,8 +1818,7 @@ - (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsWithinThresho
XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 400, 140),
[inputView firstRectForRange:multiRectRange]));
} else {
XCTAssertTrue(CGRectEqualToRect(CGRectMake(300, 0, 100, 100),
[inputView firstRectForRange:multiRectRange]));
XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
}
}

Expand Down