Skip to content

Commit f719e3d

Browse files
moffatmanJasguerrero
authored andcommitted
Fix crash with CJK keyboard with emoji at end of text field (flutter#42539)
The `isRTLAtPosition` method had a bug, it used `NSInteger max = [_selectionRects count]` instead of `NSInteger max = [_selectionRects count] - 1`. But I realized we don't even need the function any more, it was used in a few places in previous iterations of flutter#36643, but in the only place remaining, we actually already have the selection rect and don't need to search for it by position. Btw as an explanation of the crash, I guess there is some mismatch between code point and character count somewhere. UIKit was asking for `caretRectForPosition:2` when we only had 1 character. This could have only crashed when floating cursor selection was used, but actually when switching to CJK keyboard, UIKit turns out to use `caretRectForPosition` to calculate something about the composing rect. Fixes flutter/flutter#128031
1 parent 45f6e00 commit f719e3d

File tree

2 files changed

+81
-15
lines changed

2 files changed

+81
-15
lines changed

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

Lines changed: 58 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1600,22 +1600,65 @@ - (CGRect)firstRectForRange:(UITextRange*)range {
16001600
}
16011601

16021602
- (CGRect)caretRectForPosition:(UITextPosition*)position {
1603-
// TODO(cbracken) Implement.
1604-
1605-
// As of iOS 14.4, this call is used by iOS's
1606-
// _UIKeyboardTextSelectionController to determine the position
1607-
// of the floating cursor when the user force touches the space
1608-
// bar to initiate floating cursor.
1609-
//
1610-
// It is recommended to return a value that's roughly the
1611-
// center of kSpacePanBounds to make sure the floating cursor
1612-
// has ample space in all directions and does not hit kSpacePanBounds.
1613-
// See the comments in beginFloatingCursorAtPoint.
1614-
return CGRectZero;
1615-
}
1603+
NSInteger index = ((FlutterTextPosition*)position).index;
1604+
UITextStorageDirection affinity = ((FlutterTextPosition*)position).affinity;
1605+
// Get the selectionRect of the characters before and after the requested caret position.
1606+
NSArray<UITextSelectionRect*>* rects = [self
1607+
selectionRectsForRange:[FlutterTextRange
1608+
rangeWithNSRange:fml::RangeForCharactersInRange(
1609+
self.text,
1610+
NSMakeRange(
1611+
MAX(0, index - 1),
1612+
(index >= (NSInteger)self.text.length)
1613+
? 1
1614+
: 2))]];
1615+
if (rects.count == 0) {
1616+
return CGRectZero;
1617+
}
1618+
if (index == 0) {
1619+
// There is no character before the caret, so this will be the bounds of the character after the
1620+
// caret position.
1621+
CGRect characterAfterCaret = rects[0].rect;
1622+
// Return a zero-width rectangle along the upstream edge of the character after the caret
1623+
// position.
1624+
if ([rects[0] isKindOfClass:[FlutterTextSelectionRect class]] &&
1625+
((FlutterTextSelectionRect*)rects[0]).isRTL) {
1626+
return CGRectMake(characterAfterCaret.origin.x + characterAfterCaret.size.width,
1627+
characterAfterCaret.origin.y, 0, characterAfterCaret.size.height);
1628+
} else {
1629+
return CGRectMake(characterAfterCaret.origin.x, characterAfterCaret.origin.y, 0,
1630+
characterAfterCaret.size.height);
1631+
}
1632+
} else if (rects.count == 2 && affinity == UITextStorageDirectionForward) {
1633+
// There are characters before and after the caret, with forward direction affinity.
1634+
// It's better to use the character after the caret.
1635+
CGRect characterAfterCaret = rects[1].rect;
1636+
// Return a zero-width rectangle along the upstream edge of the character after the caret
1637+
// position.
1638+
if ([rects[1] isKindOfClass:[FlutterTextSelectionRect class]] &&
1639+
((FlutterTextSelectionRect*)rects[1]).isRTL) {
1640+
return CGRectMake(characterAfterCaret.origin.x + characterAfterCaret.size.width,
1641+
characterAfterCaret.origin.y, 0, characterAfterCaret.size.height);
1642+
} else {
1643+
return CGRectMake(characterAfterCaret.origin.x, characterAfterCaret.origin.y, 0,
1644+
characterAfterCaret.size.height);
1645+
}
1646+
}
16161647

1617-
- (CGRect)bounds {
1618-
return _isFloatingCursorActive ? kSpacePanBounds : super.bounds;
1648+
// Covers 2 remaining cases:
1649+
// 1. there are characters before and after the caret, with backward direction affinity.
1650+
// 2. there is only 1 character before the caret (caret is at the end of text).
1651+
// For both cases, return a zero-width rectangle along the downstream edge of the character
1652+
// before the caret position.
1653+
CGRect characterBeforeCaret = rects[0].rect;
1654+
if ([rects[0] isKindOfClass:[FlutterTextSelectionRect class]] &&
1655+
((FlutterTextSelectionRect*)rects[0]).isRTL) {
1656+
return CGRectMake(characterBeforeCaret.origin.x, characterBeforeCaret.origin.y, 0,
1657+
characterBeforeCaret.size.height);
1658+
} else {
1659+
return CGRectMake(characterBeforeCaret.origin.x + characterBeforeCaret.size.width,
1660+
characterBeforeCaret.origin.y, 0, characterBeforeCaret.size.height);
1661+
}
16191662
}
16201663

16211664
- (UITextPosition*)closestPositionToPoint:(CGPoint)point {

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1429,6 +1429,29 @@ - (void)testClosestPositionToPointWithinRange {
14291429
1U, ((FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).index);
14301430
}
14311431

1432+
- (void)testClosestPositionToPointWithPartialSelectionRects {
1433+
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1434+
[inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1435+
1436+
[inputView setSelectionRects:@[ [FlutterTextSelectionRect
1437+
selectionRectWithRect:CGRectMake(0, 0, 100, 100)
1438+
position:0U] ]];
1439+
// Asking with a position at the end of selection rects should give you the trailing edge of
1440+
// the last rect.
1441+
XCTAssertTrue(CGRectEqualToRect(
1442+
[inputView caretRectForPosition:[FlutterTextPosition
1443+
positionWithIndex:1
1444+
affinity:UITextStorageDirectionForward]],
1445+
CGRectMake(100, 0, 0, 100)));
1446+
// Asking with a position beyond the end of selection rects should return CGRectZero without
1447+
// crashing.
1448+
XCTAssertTrue(CGRectEqualToRect(
1449+
[inputView caretRectForPosition:[FlutterTextPosition
1450+
positionWithIndex:2
1451+
affinity:UITextStorageDirectionForward]],
1452+
CGRectZero));
1453+
}
1454+
14321455
#pragma mark - Floating Cursor - Tests
14331456

14341457
- (void)testFloatingCursorDoesNotThrow {

0 commit comments

Comments
 (0)