Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit d532b47

Browse files
committed
update for varying selection rect size
1 parent a01f9b3 commit d532b47

File tree

2 files changed

+166
-17
lines changed

2 files changed

+166
-17
lines changed

shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1676,6 +1676,10 @@ - (CGRect)firstRectForRange:(UITextRange*)range {
16761676

16771677
CGRect startSelectionRect = CGRectNull;
16781678
CGRect endSelectionRect = CGRectNull;
1679+
// Selection rects from different langauges may have different minY/maxY.
1680+
// So we need to iterate through each rects to update minY/maxY.
1681+
CGFloat minY = CGFLOAT_MAX;
1682+
CGFloat maxY = CGFLOAT_MIN;
16791683

16801684
FlutterTextRange* textRange = [FlutterTextRange
16811685
rangeWithNSRange:fml::RangeForCharactersInRange(self.text, NSMakeRange(0, self.text.length))];
@@ -1694,17 +1698,19 @@ - (CGRect)firstRectForRange:(UITextRange*)range {
16941698
return _selectionRects[i].rect;
16951699
}
16961700
}
1697-
// TODO(hellohaunlin): Remove iOS 17 check. The logic should also work for older versions.
1698-
if (@available(iOS 17, *)) {
1699-
if (!CGRectIsNull(startSelectionRect)) {
1700-
BOOL endsOnOrAfterEndOfRange = _selectionRects[i].position >= end - 1; // end is exclusive
1701-
BOOL nextSelectRectIsOnNextLine =
1702-
!isLastSelectionRect &&
1703-
_selectionRects[i + 1].rect.origin.y != _selectionRects[i].rect.origin.y;
1704-
if (endsOnOrAfterEndOfRange || isLastSelectionRect || nextSelectRectIsOnNextLine) {
1705-
endSelectionRect = _selectionRects[i].rect;
1706-
break;
1707-
}
1701+
if (!CGRectIsNull(startSelectionRect)) {
1702+
minY = fmin(minY, CGRectGetMinY(_selectionRects[i].rect));
1703+
maxY = fmax(maxY, CGRectGetMaxY(_selectionRects[i].rect));
1704+
BOOL endsOnOrAfterEndOfRange = _selectionRects[i].position >= end - 1; // end is exclusive
1705+
BOOL nextSelectionRectIsOnNextLine =
1706+
!isLastSelectionRect &&
1707+
// Seleciton rects from different langauges in 2 lines may overlap with each other.
1708+
// A good approximation is to check if the center of next rect is below the bottom of
1709+
// current rect.
1710+
CGRectGetMidY(_selectionRects[i + 1].rect) > CGRectGetMaxY(_selectionRects[i].rect);
1711+
if (endsOnOrAfterEndOfRange || isLastSelectionRect || nextSelectionRectIsOnNextLine) {
1712+
endSelectionRect = _selectionRects[i].rect;
1713+
break;
17081714
}
17091715
}
17101716
}
@@ -1714,8 +1720,7 @@ - (CGRect)firstRectForRange:(UITextRange*)range {
17141720
// fmin/fmax to support both LTR and RTL languages.
17151721
CGFloat minX = fmin(CGRectGetMinX(startSelectionRect), CGRectGetMinX(endSelectionRect));
17161722
CGFloat maxX = fmax(CGRectGetMaxX(startSelectionRect), CGRectGetMaxX(endSelectionRect));
1717-
return CGRectMake(minX, startSelectionRect.origin.y, maxX - minX,
1718-
startSelectionRect.size.height);
1723+
return CGRectMake(minX, minY, maxX - minX, maxY - minY);
17191724
}
17201725
}
17211726

shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm

Lines changed: 148 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1543,7 +1543,7 @@ - (void)testUpdateFirstRectForRange {
15431543
[inputView firstRectForRange:range]));
15441544
}
15451545

1546-
- (void)testFirstRectForRangeReturnsCorrectSelectionRectOnASingleLineLeftToRight {
1546+
- (void)testFirstRectForRangeReturnsCorrectRectOnASingleLineLeftToRight {
15471547
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
15481548
[inputView setTextInputState:@{@"text" : @"COMPOSING"}];
15491549

@@ -1572,7 +1572,7 @@ - (void)testFirstRectForRangeReturnsCorrectSelectionRectOnASingleLineLeftToRight
15721572
XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:rangeOutsideBounds]));
15731573
}
15741574

1575-
- (void)testFirstRectForRangeReturnsCorrectSelectionRectOnASingleLineRightToLeft {
1575+
- (void)testFirstRectForRangeReturnsCorrectRectOnASingleLineRightToLeft {
15761576
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
15771577
[inputView setTextInputState:@{@"text" : @"COMPOSING"}];
15781578

@@ -1600,7 +1600,7 @@ - (void)testFirstRectForRangeReturnsCorrectSelectionRectOnASingleLineRightToLeft
16001600
XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:rangeOutsideBounds]));
16011601
}
16021602

1603-
- (void)testFirstRectForRangeReturnsCorrectSelectionRectOnMultipleLinesLeftToRight {
1603+
- (void)testFirstRectForRangeReturnsCorrectRectOnMultipleLinesLeftToRight {
16041604
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
16051605
[inputView setTextInputState:@{@"text" : @"COMPOSING"}];
16061606

@@ -1629,7 +1629,7 @@ - (void)testFirstRectForRangeReturnsCorrectSelectionRectOnMultipleLinesLeftToRig
16291629
}
16301630
}
16311631

1632-
- (void)testFirstRectForRangeReturnsCorrectSelectionRectOnMultipleLinesRightToLeft {
1632+
- (void)testFirstRectForRangeReturnsCorrectRectOnMultipleLinesRightToLeft {
16331633
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
16341634
[inputView setTextInputState:@{@"text" : @"COMPOSING"}];
16351635

@@ -1657,6 +1657,150 @@ - (void)testFirstRectForRangeReturnsCorrectSelectionRectOnMultipleLinesRightToLe
16571657
}
16581658
}
16591659

1660+
- (void)testFirstRectForRangeReturnsCorrectRectOnSingleLineWithVaryingMinYAndMaxYLeftToRight {
1661+
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1662+
[inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1663+
1664+
[inputView setSelectionRects:@[
1665+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:0U],
1666+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 10, 100, 80)
1667+
position:1U], // shorter
1668+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, -10, 100, 120)
1669+
position:2U], // taller
1670+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 0, 100, 100) position:3U],
1671+
]];
1672+
1673+
FlutterTextRange* multiRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 3)];
1674+
1675+
if (@available(iOS 17, *)) {
1676+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, -10, 300, 120),
1677+
[inputView firstRectForRange:multiRectRange]));
1678+
} else {
1679+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 10, 100, 80),
1680+
[inputView firstRectForRange:multiRectRange]));
1681+
}
1682+
}
1683+
1684+
- (void)testFirstRectForRangeReturnsCorrectRectOnSingleLineWithVaryingMinYAndMaxYRightToLeft {
1685+
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1686+
[inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1687+
1688+
[inputView setSelectionRects:@[
1689+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 0, 100, 100) position:0U],
1690+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, -10, 100, 120)
1691+
position:1U], // taller
1692+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 10, 100, 80)
1693+
position:2U], // shorter
1694+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:3U],
1695+
]];
1696+
1697+
FlutterTextRange* multiRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 3)];
1698+
1699+
if (@available(iOS 17, *)) {
1700+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, -10, 300, 120),
1701+
[inputView firstRectForRange:multiRectRange]));
1702+
} else {
1703+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, -10, 100, 120),
1704+
[inputView firstRectForRange:multiRectRange]));
1705+
}
1706+
}
1707+
1708+
- (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsExceedingThresholdLeftToRight {
1709+
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1710+
[inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1711+
1712+
[inputView setSelectionRects:@[
1713+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:0U],
1714+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 0, 100, 100) position:1U],
1715+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 0, 100, 100) position:2U],
1716+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 0, 100, 100) position:3U],
1717+
// y=60 exceeds threshold, so treat it as a new line.
1718+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 60, 100, 100) position:4U],
1719+
]];
1720+
1721+
FlutterTextRange* multiRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 4)];
1722+
1723+
if (@available(iOS 17, *)) {
1724+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1725+
[inputView firstRectForRange:multiRectRange]));
1726+
} else {
1727+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1728+
[inputView firstRectForRange:multiRectRange]));
1729+
}
1730+
}
1731+
1732+
- (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsExceedingThresholdRightToLeft {
1733+
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1734+
[inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1735+
1736+
[inputView setSelectionRects:@[
1737+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 0, 100, 100) position:0U],
1738+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 0, 100, 100) position:1U],
1739+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 0, 100, 100) position:2U],
1740+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:3U],
1741+
// y=60 exceeds threshold, so treat it as a new line.
1742+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 60, 100, 100) position:4U],
1743+
]];
1744+
1745+
FlutterTextRange* multiRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 4)];
1746+
1747+
if (@available(iOS 17, *)) {
1748+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
1749+
[inputView firstRectForRange:multiRectRange]));
1750+
} else {
1751+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
1752+
[inputView firstRectForRange:multiRectRange]));
1753+
}
1754+
}
1755+
1756+
- (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsWithinThresholdLeftToRight {
1757+
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1758+
[inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1759+
1760+
[inputView setSelectionRects:@[
1761+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:0U],
1762+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 0, 100, 100) position:1U],
1763+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 0, 100, 100) position:2U],
1764+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 0, 100, 100) position:3U],
1765+
// y=40 is within line threshold, so treat it as the same line
1766+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(400, 40, 100, 100) position:4U],
1767+
]];
1768+
1769+
FlutterTextRange* multiRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 4)];
1770+
1771+
if (@available(iOS 17, *)) {
1772+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 400, 140),
1773+
[inputView firstRectForRange:multiRectRange]));
1774+
} else {
1775+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1776+
[inputView firstRectForRange:multiRectRange]));
1777+
}
1778+
}
1779+
1780+
- (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsWithinThresholdRightToLeft {
1781+
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1782+
[inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1783+
1784+
[inputView setSelectionRects:@[
1785+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(400, 0, 100, 100) position:0U],
1786+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 0, 100, 100) position:1U],
1787+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 0, 100, 100) position:2U],
1788+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 0, 100, 100) position:3U],
1789+
// y=40 is within line threshold, so treat it as the same line
1790+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 40, 100, 100) position:4U],
1791+
]];
1792+
1793+
FlutterTextRange* multiRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 4)];
1794+
1795+
if (@available(iOS 17, *)) {
1796+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 400, 140),
1797+
[inputView firstRectForRange:multiRectRange]));
1798+
} else {
1799+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(300, 0, 100, 100),
1800+
[inputView firstRectForRange:multiRectRange]));
1801+
}
1802+
}
1803+
16601804
- (void)testClosestPositionToPoint {
16611805
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
16621806
[inputView setTextInputState:@{@"text" : @"COMPOSING"}];

0 commit comments

Comments
 (0)