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

Commit 9b7de88

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

File tree

2 files changed

+138
-9
lines changed

2 files changed

+138
-9
lines changed

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

Lines changed: 36 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++) {
@@ -1679,13 +1685,40 @@ - (CGRect)firstRectForRange:(UITextRange*)range {
16791685
BOOL endOfTextIsAfterStartOfRange = isLastSelectionRect && textRange.range.length > first;
16801686
BOOL nextSelectionRectIsAfterStartOfRange =
16811687
!isLastSelectionRect && _selectionRects[i + 1].position > first;
1688+
16821689
if (startsOnOrBeforeStartOfRange &&
16831690
(endOfTextIsAfterStartOfRange || nextSelectionRectIsAfterStartOfRange)) {
1684-
return _selectionRects[i].rect;
1691+
// TODO(hellohaunlin): remove iOS 17 checks. The iOS 17 logic should apply to earlier
1692+
// versions.
1693+
if (@available(iOS 17, *)) {
1694+
startSelectionRect = _selectionRects[i].rect;
1695+
} else {
1696+
return _selectionRects[i].rect;
1697+
}
1698+
}
1699+
// TODO(hellohaunlin): remove iOS 17 checks. The iOS 17 logic should apply to earlier versions.
1700+
if (@available(iOS 17, *)) {
1701+
if (!CGRectIsNull(startSelectionRect)) {
1702+
BOOL endsOnOrAfterEndOfRange = _selectionRects[i].position >= end - 1; // end is exclusive
1703+
BOOL nextSelectRectIsOnNextLine =
1704+
!isLastSelectionRect &&
1705+
_selectionRects[i + 1].rect.origin.y != _selectionRects[i].rect.origin.y;
1706+
if (endsOnOrAfterEndOfRange || isLastSelectionRect || nextSelectRectIsOnNextLine) {
1707+
endSelectionRect = _selectionRects[i].rect;
1708+
break;
1709+
}
1710+
}
16851711
}
16861712
}
1687-
1688-
return CGRectZero;
1713+
if (CGRectIsNull(startSelectionRect) || CGRectIsNull(endSelectionRect)) {
1714+
return CGRectZero;
1715+
} else {
1716+
// fmin/max to support both LTR and RTL languages.
1717+
CGFloat minX = fmin(CGRectGetMinX(startSelectionRect), CGRectGetMinX(endSelectionRect));
1718+
CGFloat maxX = fmax(CGRectGetMaxX(startSelectionRect), CGRectGetMaxX(endSelectionRect));
1719+
return CGRectMake(minX, startSelectionRect.origin.y, maxX - minX,
1720+
startSelectionRect.size.height);
1721+
}
16891722
}
16901723

16911724
- (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)