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

Commit 17a711a

Browse files
authored
[CP][ios][text_input_highlight]fix text input system highlight in iOS 17 Beta 7 with firstRectForRange (#45398)
original PR: #45303 *Replace this paragraph with a description of what this PR is changing or adding, and why. Consider including before/after screenshots.* *List which issues are fixed by this PR. You must list at least one issue.* flutter/flutter#131622 *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].* [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
1 parent fec13df commit 17a711a

File tree

2 files changed

+286
-9
lines changed

2 files changed

+286
-9
lines changed

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

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1598,6 +1598,8 @@ - (CGRect)localRectFromFrameworkTransform:(CGRect)incomingRect {
15981598
// and to position the
15991599
// candidates view for multi-stage input methods (e.g., Japanese) when using a
16001600
// physical keyboard.
1601+
// Returns the rect for the queried range, or a subrange through the end of line, if
1602+
// the range encompasses multiple lines.
16011603
- (CGRect)firstRectForRange:(UITextRange*)range {
16021604
NSAssert([range.start isKindOfClass:[FlutterTextPosition class]],
16031605
@"Expected a FlutterTextPosition for range.start (got %@).", [range.start class]);
@@ -1652,6 +1654,14 @@ - (CGRect)firstRectForRange:(UITextRange*)range {
16521654
if (end < start) {
16531655
first = end;
16541656
}
1657+
1658+
CGRect startSelectionRect = CGRectNull;
1659+
CGRect endSelectionRect = CGRectNull;
1660+
// Selection rects from different langauges may have different minY/maxY.
1661+
// So we need to iterate through each rects to update minY/maxY.
1662+
CGFloat minY = CGFLOAT_MAX;
1663+
CGFloat maxY = CGFLOAT_MIN;
1664+
16551665
FlutterTextRange* textRange = [FlutterTextRange
16561666
rangeWithNSRange:fml::RangeForCharactersInRange(self.text, NSMakeRange(0, self.text.length))];
16571667
for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
@@ -1662,11 +1672,38 @@ - (CGRect)firstRectForRange:(UITextRange*)range {
16621672
!isLastSelectionRect && _selectionRects[i + 1].position > first;
16631673
if (startsOnOrBeforeStartOfRange &&
16641674
(endOfTextIsAfterStartOfRange || nextSelectionRectIsAfterStartOfRange)) {
1665-
return _selectionRects[i].rect;
1675+
// TODO(hellohaunlin): Remove iOS 17 check. The logic should also work for older versions.
1676+
if (@available(iOS 17, *)) {
1677+
startSelectionRect = _selectionRects[i].rect;
1678+
} else {
1679+
return _selectionRects[i].rect;
1680+
}
1681+
}
1682+
if (!CGRectIsNull(startSelectionRect)) {
1683+
minY = fmin(minY, CGRectGetMinY(_selectionRects[i].rect));
1684+
maxY = fmax(maxY, CGRectGetMaxY(_selectionRects[i].rect));
1685+
BOOL endsOnOrAfterEndOfRange = _selectionRects[i].position >= end - 1; // end is exclusive
1686+
BOOL nextSelectionRectIsOnNextLine =
1687+
!isLastSelectionRect &&
1688+
// Selection rects from different langauges in 2 lines may overlap with each other.
1689+
// A good approximation is to check if the center of next rect is below the bottom of
1690+
// current rect.
1691+
// TODO(hellohuanlin): Consider passing the line break info from framework.
1692+
CGRectGetMidY(_selectionRects[i + 1].rect) > CGRectGetMaxY(_selectionRects[i].rect);
1693+
if (endsOnOrAfterEndOfRange || isLastSelectionRect || nextSelectionRectIsOnNextLine) {
1694+
endSelectionRect = _selectionRects[i].rect;
1695+
break;
1696+
}
16661697
}
16671698
}
1668-
1669-
return CGRectZero;
1699+
if (CGRectIsNull(startSelectionRect) || CGRectIsNull(endSelectionRect)) {
1700+
return CGRectZero;
1701+
} else {
1702+
// fmin/fmax to support both LTR and RTL languages.
1703+
CGFloat minX = fmin(CGRectGetMinX(startSelectionRect), CGRectGetMinX(endSelectionRect));
1704+
CGFloat maxX = fmax(CGRectGetMaxX(startSelectionRect), CGRectGetMaxX(endSelectionRect));
1705+
return CGRectMake(minX, minY, maxX - minX, maxY - minY);
1706+
}
16701707
}
16711708

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

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

Lines changed: 246 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1438,24 +1438,264 @@ - (void)testUpdateFirstRectForRange {
14381438
[inputView firstRectForRange:range]));
14391439
}
14401440

1441-
- (void)testFirstRectForRangeReturnsCorrectSelectionRect {
1441+
- (void)testFirstRectForRangeReturnsCorrectRectOnASingleLineLeftToRight {
14421442
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
14431443
[inputView setTextInputState:@{@"text" : @"COMPOSING"}];
14441444

1445-
FlutterTextRange* range = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 1)];
1446-
CGRect testRect = CGRectMake(100, 100, 100, 100);
14471445
[inputView setSelectionRects:@[
14481446
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:0U],
1449-
[FlutterTextSelectionRect selectionRectWithRect:testRect position:1U],
1450-
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 200, 100, 100) position:2U],
1447+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 0, 100, 100) position:1U],
1448+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 0, 100, 100) position:2U],
1449+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 0, 100, 100) position:3U],
14511450
]];
1452-
XCTAssertTrue(CGRectEqualToRect(testRect, [inputView firstRectForRange:range]));
1451+
FlutterTextRange* singleRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 1)];
1452+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1453+
[inputView firstRectForRange:singleRectRange]));
1454+
1455+
FlutterTextRange* multiRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 3)];
1456+
1457+
if (@available(iOS 17, *)) {
1458+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1459+
[inputView firstRectForRange:multiRectRange]));
1460+
} else {
1461+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1462+
[inputView firstRectForRange:multiRectRange]));
1463+
}
14531464

14541465
[inputView setTextInputState:@{@"text" : @"COM"}];
14551466
FlutterTextRange* rangeOutsideBounds = [FlutterTextRange rangeWithNSRange:NSMakeRange(3, 1)];
14561467
XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:rangeOutsideBounds]));
14571468
}
14581469

1470+
- (void)testFirstRectForRangeReturnsCorrectRectOnASingleLineRightToLeft {
1471+
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1472+
[inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1473+
1474+
[inputView setSelectionRects:@[
1475+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 0, 100, 100) position:0U],
1476+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 0, 100, 100) position:1U],
1477+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 0, 100, 100) position:2U],
1478+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:3U],
1479+
]];
1480+
FlutterTextRange* singleRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 1)];
1481+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
1482+
[inputView firstRectForRange:singleRectRange]));
1483+
1484+
FlutterTextRange* multiRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 3)];
1485+
if (@available(iOS 17, *)) {
1486+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
1487+
[inputView firstRectForRange:multiRectRange]));
1488+
} else {
1489+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
1490+
[inputView firstRectForRange:multiRectRange]));
1491+
}
1492+
1493+
[inputView setTextInputState:@{@"text" : @"COM"}];
1494+
FlutterTextRange* rangeOutsideBounds = [FlutterTextRange rangeWithNSRange:NSMakeRange(3, 1)];
1495+
XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:rangeOutsideBounds]));
1496+
}
1497+
1498+
- (void)testFirstRectForRangeReturnsCorrectRectOnMultipleLinesLeftToRight {
1499+
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1500+
[inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1501+
1502+
[inputView setSelectionRects:@[
1503+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:0U],
1504+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 0, 100, 100) position:1U],
1505+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 0, 100, 100) position:2U],
1506+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 0, 100, 100) position:3U],
1507+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 100, 100, 100) position:4U],
1508+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 100, 100, 100) position:5U],
1509+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 100, 100, 100) position:6U],
1510+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 100, 100, 100) position:7U],
1511+
]];
1512+
FlutterTextRange* singleRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 1)];
1513+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1514+
[inputView firstRectForRange:singleRectRange]));
1515+
1516+
FlutterTextRange* multiRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 4)];
1517+
1518+
if (@available(iOS 17, *)) {
1519+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1520+
[inputView firstRectForRange:multiRectRange]));
1521+
} else {
1522+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1523+
[inputView firstRectForRange:multiRectRange]));
1524+
}
1525+
}
1526+
1527+
- (void)testFirstRectForRangeReturnsCorrectRectOnMultipleLinesRightToLeft {
1528+
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1529+
[inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1530+
1531+
[inputView setSelectionRects:@[
1532+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 0, 100, 100) position:0U],
1533+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 0, 100, 100) position:1U],
1534+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 0, 100, 100) position:2U],
1535+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:3U],
1536+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 100, 100, 100) position:4U],
1537+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 100, 100, 100) position:5U],
1538+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 100, 100, 100) position:6U],
1539+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 100, 100, 100) position:7U],
1540+
]];
1541+
FlutterTextRange* singleRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 1)];
1542+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
1543+
[inputView firstRectForRange:singleRectRange]));
1544+
1545+
FlutterTextRange* multiRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 4)];
1546+
if (@available(iOS 17, *)) {
1547+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
1548+
[inputView firstRectForRange:multiRectRange]));
1549+
} else {
1550+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
1551+
[inputView firstRectForRange:multiRectRange]));
1552+
}
1553+
}
1554+
1555+
- (void)testFirstRectForRangeReturnsCorrectRectOnSingleLineWithVaryingMinYAndMaxYLeftToRight {
1556+
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1557+
[inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1558+
1559+
[inputView setSelectionRects:@[
1560+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:0U],
1561+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 10, 100, 80)
1562+
position:1U], // shorter
1563+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, -10, 100, 120)
1564+
position:2U], // taller
1565+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 0, 100, 100) position:3U],
1566+
]];
1567+
1568+
FlutterTextRange* multiRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 3)];
1569+
1570+
if (@available(iOS 17, *)) {
1571+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, -10, 300, 120),
1572+
[inputView firstRectForRange:multiRectRange]));
1573+
} else {
1574+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 10, 100, 80),
1575+
[inputView firstRectForRange:multiRectRange]));
1576+
}
1577+
}
1578+
1579+
- (void)testFirstRectForRangeReturnsCorrectRectOnSingleLineWithVaryingMinYAndMaxYRightToLeft {
1580+
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1581+
[inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1582+
1583+
[inputView setSelectionRects:@[
1584+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 0, 100, 100) position:0U],
1585+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, -10, 100, 120)
1586+
position:1U], // taller
1587+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 10, 100, 80)
1588+
position:2U], // shorter
1589+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:3U],
1590+
]];
1591+
1592+
FlutterTextRange* multiRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 3)];
1593+
1594+
if (@available(iOS 17, *)) {
1595+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, -10, 300, 120),
1596+
[inputView firstRectForRange:multiRectRange]));
1597+
} else {
1598+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, -10, 100, 120),
1599+
[inputView firstRectForRange:multiRectRange]));
1600+
}
1601+
}
1602+
1603+
- (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsExceedingThresholdLeftToRight {
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+
// y=60 exceeds threshold, so treat it as a new line.
1613+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 60, 100, 100) position:4U],
1614+
]];
1615+
1616+
FlutterTextRange* multiRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 4)];
1617+
1618+
if (@available(iOS 17, *)) {
1619+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1620+
[inputView firstRectForRange:multiRectRange]));
1621+
} else {
1622+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1623+
[inputView firstRectForRange:multiRectRange]));
1624+
}
1625+
}
1626+
1627+
- (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsExceedingThresholdRightToLeft {
1628+
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1629+
[inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1630+
1631+
[inputView setSelectionRects:@[
1632+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 0, 100, 100) position:0U],
1633+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 0, 100, 100) position:1U],
1634+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 0, 100, 100) position:2U],
1635+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:3U],
1636+
// y=60 exceeds threshold, so treat it as a new line.
1637+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 60, 100, 100) position:4U],
1638+
]];
1639+
1640+
FlutterTextRange* multiRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 4)];
1641+
1642+
if (@available(iOS 17, *)) {
1643+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
1644+
[inputView firstRectForRange:multiRectRange]));
1645+
} else {
1646+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
1647+
[inputView firstRectForRange:multiRectRange]));
1648+
}
1649+
}
1650+
1651+
- (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsWithinThresholdLeftToRight {
1652+
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1653+
[inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1654+
1655+
[inputView setSelectionRects:@[
1656+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:0U],
1657+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 0, 100, 100) position:1U],
1658+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 0, 100, 100) position:2U],
1659+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 0, 100, 100) position:3U],
1660+
// y=40 is within line threshold, so treat it as the same line
1661+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(400, 40, 100, 100) position:4U],
1662+
]];
1663+
1664+
FlutterTextRange* multiRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 4)];
1665+
1666+
if (@available(iOS 17, *)) {
1667+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 400, 140),
1668+
[inputView firstRectForRange:multiRectRange]));
1669+
} else {
1670+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1671+
[inputView firstRectForRange:multiRectRange]));
1672+
}
1673+
}
1674+
1675+
- (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsWithinThresholdRightToLeft {
1676+
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1677+
[inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1678+
1679+
[inputView setSelectionRects:@[
1680+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(400, 0, 100, 100) position:0U],
1681+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 0, 100, 100) position:1U],
1682+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 0, 100, 100) position:2U],
1683+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 0, 100, 100) position:3U],
1684+
// y=40 is within line threshold, so treat it as the same line
1685+
[FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 40, 100, 100) position:4U],
1686+
]];
1687+
1688+
FlutterTextRange* multiRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 4)];
1689+
1690+
if (@available(iOS 17, *)) {
1691+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 400, 140),
1692+
[inputView firstRectForRange:multiRectRange]));
1693+
} else {
1694+
XCTAssertTrue(CGRectEqualToRect(CGRectMake(300, 0, 100, 100),
1695+
[inputView firstRectForRange:multiRectRange]));
1696+
}
1697+
}
1698+
14591699
- (void)testClosestPositionToPoint {
14601700
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
14611701
[inputView setTextInputState:@{@"text" : @"COMPOSING"}];

0 commit comments

Comments
 (0)