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

Commit a01f9b3

Browse files
committed
[ios][text_input_highlight]fix firstRectForRange
1 parent 62cf36e commit a01f9b3

2 files changed

Lines changed: 136 additions & 9 deletions

File tree

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

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1617,6 +1617,8 @@ - (CGRect)localRectFromFrameworkTransform:(CGRect)incomingRect {
16171617
// and to position the
16181618
// candidates view for multi-stage input methods (e.g., Japanese) when using a
16191619
// physical keyboard.
1620+
// Returns the rect for the queried range, or a subrange through the end of line, if
1621+
// the range encompasses multiple lines.
16201622
- (CGRect)firstRectForRange:(UITextRange*)range {
16211623
NSAssert([range.start isKindOfClass:[FlutterTextPosition class]],
16221624
@"Expected a FlutterTextPosition for range.start (got %@).", [range.start class]);
@@ -1671,6 +1673,10 @@ - (CGRect)firstRectForRange:(UITextRange*)range {
16711673
if (end < start) {
16721674
first = end;
16731675
}
1676+
1677+
CGRect startSelectionRect = CGRectNull;
1678+
CGRect endSelectionRect = CGRectNull;
1679+
16741680
FlutterTextRange* textRange = [FlutterTextRange
16751681
rangeWithNSRange:fml::RangeForCharactersInRange(self.text, NSMakeRange(0, self.text.length))];
16761682
for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
@@ -1681,11 +1687,36 @@ - (CGRect)firstRectForRange:(UITextRange*)range {
16811687
!isLastSelectionRect && _selectionRects[i + 1].position > first;
16821688
if (startsOnOrBeforeStartOfRange &&
16831689
(endOfTextIsAfterStartOfRange || nextSelectionRectIsAfterStartOfRange)) {
1684-
return _selectionRects[i].rect;
1690+
// TODO(hellohaunlin): Remove iOS 17 check. The logic should also work for older versions.
1691+
if (@available(iOS 17, *)) {
1692+
startSelectionRect = _selectionRects[i].rect;
1693+
} else {
1694+
return _selectionRects[i].rect;
1695+
}
1696+
}
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+
}
1708+
}
16851709
}
16861710
}
1687-
1688-
return CGRectZero;
1711+
if (CGRectIsNull(startSelectionRect) || CGRectIsNull(endSelectionRect)) {
1712+
return CGRectZero;
1713+
} else {
1714+
// fmin/fmax to support both LTR and RTL languages.
1715+
CGFloat minX = fmin(CGRectGetMinX(startSelectionRect), CGRectGetMinX(endSelectionRect));
1716+
CGFloat maxX = fmax(CGRectGetMaxX(startSelectionRect), CGRectGetMaxX(endSelectionRect));
1717+
return CGRectMake(minX, startSelectionRect.origin.y, maxX - minX,
1718+
startSelectionRect.size.height);
1719+
}
16891720
}
16901721

16911722
- (CGRect)caretRectForPosition:(UITextPosition*)position {

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

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

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

1550-
FlutterTextRange* range = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 1)];
1551-
CGRect testRect = CGRectMake(100, 100, 100, 100);
15521550
[inputView setSelectionRects:@[
15531551
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:0U],
1554-
[FlutterTextSelectionRect selectionRectWithRect:testRect position:1U],
1555-
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 200, 100, 100) position:2U],
1552+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 0, 100, 100) position:1U],
1553+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 0, 100, 100) position:2U],
1554+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 0, 100, 100) position:3U],
15561555
]];
1557-
XCTAssertTrue(CGRectEqualToRect(testRect, [inputView firstRectForRange:range]));
1556+
FlutterTextRange* singleRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 1)];
1557+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1558+
[inputView firstRectForRange:singleRectRange]));
1559+
1560+
FlutterTextRange* multiRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 3)];
1561+
1562+
if (@available(iOS 17, *)) {
1563+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1564+
[inputView firstRectForRange:multiRectRange]));
1565+
} else {
1566+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1567+
[inputView firstRectForRange:multiRectRange]));
1568+
}
15581569

15591570
[inputView setTextInputState:@{@"text" : @"COM"}];
15601571
FlutterTextRange* rangeOutsideBounds = [FlutterTextRange rangeWithNSRange:NSMakeRange(3, 1)];
15611572
XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:rangeOutsideBounds]));
15621573
}
15631574

1575+
- (void)testFirstRectForRangeReturnsCorrectSelectionRectOnASingleLineRightToLeft {
1576+
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1577+
[inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1578+
1579+
[inputView setSelectionRects:@[
1580+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 0, 100, 100) position:0U],
1581+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 0, 100, 100) position:1U],
1582+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 0, 100, 100) position:2U],
1583+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:3U],
1584+
]];
1585+
FlutterTextRange* singleRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 1)];
1586+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
1587+
[inputView firstRectForRange:singleRectRange]));
1588+
1589+
FlutterTextRange* multiRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 3)];
1590+
if (@available(iOS 17, *)) {
1591+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
1592+
[inputView firstRectForRange:multiRectRange]));
1593+
} else {
1594+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
1595+
[inputView firstRectForRange:multiRectRange]));
1596+
}
1597+
1598+
[inputView setTextInputState:@{@"text" : @"COM"}];
1599+
FlutterTextRange* rangeOutsideBounds = [FlutterTextRange rangeWithNSRange:NSMakeRange(3, 1)];
1600+
XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:rangeOutsideBounds]));
1601+
}
1602+
1603+
- (void)testFirstRectForRangeReturnsCorrectSelectionRectOnMultipleLinesLeftToRight {
1604+
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1605+
[inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1606+
1607+
[inputView setSelectionRects:@[
1608+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:0U],
1609+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 0, 100, 100) position:1U],
1610+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 0, 100, 100) position:2U],
1611+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 0, 100, 100) position:3U],
1612+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 100, 100, 100) position:4U],
1613+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 100, 100, 100) position:5U],
1614+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 100, 100, 100) position:6U],
1615+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 100, 100, 100) position:7U],
1616+
]];
1617+
FlutterTextRange* singleRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 1)];
1618+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1619+
[inputView firstRectForRange:singleRectRange]));
1620+
1621+
FlutterTextRange* multiRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 4)];
1622+
1623+
if (@available(iOS 17, *)) {
1624+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1625+
[inputView firstRectForRange:multiRectRange]));
1626+
} else {
1627+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1628+
[inputView firstRectForRange:multiRectRange]));
1629+
}
1630+
}
1631+
1632+
- (void)testFirstRectForRangeReturnsCorrectSelectionRectOnMultipleLinesRightToLeft {
1633+
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1634+
[inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1635+
1636+
[inputView setSelectionRects:@[
1637+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 0, 100, 100) position:0U],
1638+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 0, 100, 100) position:1U],
1639+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 0, 100, 100) position:2U],
1640+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:3U],
1641+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 100, 100, 100) position:4U],
1642+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 100, 100, 100) position:5U],
1643+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 100, 100, 100) position:6U],
1644+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 100, 100, 100) position:7U],
1645+
]];
1646+
FlutterTextRange* singleRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 1)];
1647+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
1648+
[inputView firstRectForRange:singleRectRange]));
1649+
1650+
FlutterTextRange* multiRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 4)];
1651+
if (@available(iOS 17, *)) {
1652+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
1653+
[inputView firstRectForRange:multiRectRange]));
1654+
} else {
1655+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
1656+
[inputView firstRectForRange:multiRectRange]));
1657+
}
1658+
}
1659+
15641660
- (void)testClosestPositionToPoint {
15651661
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
15661662
[inputView setTextInputState:@{@"text" : @"COMPOSING"}];

0 commit comments

Comments
 (0)