diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm index 1ac316e819ab4..e5c152a5dc84e 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm @@ -1600,22 +1600,65 @@ - (CGRect)firstRectForRange:(UITextRange*)range { } - (CGRect)caretRectForPosition:(UITextPosition*)position { - // TODO(cbracken) Implement. - - // As of iOS 14.4, this call is used by iOS's - // _UIKeyboardTextSelectionController to determine the position - // of the floating cursor when the user force touches the space - // bar to initiate floating cursor. - // - // It is recommended to return a value that's roughly the - // center of kSpacePanBounds to make sure the floating cursor - // has ample space in all directions and does not hit kSpacePanBounds. - // See the comments in beginFloatingCursorAtPoint. - return CGRectZero; -} + NSInteger index = ((FlutterTextPosition*)position).index; + UITextStorageDirection affinity = ((FlutterTextPosition*)position).affinity; + // Get the selectionRect of the characters before and after the requested caret position. + NSArray* rects = [self + selectionRectsForRange:[FlutterTextRange + rangeWithNSRange:fml::RangeForCharactersInRange( + self.text, + NSMakeRange( + MAX(0, index - 1), + (index >= (NSInteger)self.text.length) + ? 1 + : 2))]]; + if (rects.count == 0) { + return CGRectZero; + } + if (index == 0) { + // There is no character before the caret, so this will be the bounds of the character after the + // caret position. + CGRect characterAfterCaret = rects[0].rect; + // Return a zero-width rectangle along the upstream edge of the character after the caret + // position. + if ([rects[0] isKindOfClass:[FlutterTextSelectionRect class]] && + ((FlutterTextSelectionRect*)rects[0]).isRTL) { + return CGRectMake(characterAfterCaret.origin.x + characterAfterCaret.size.width, + characterAfterCaret.origin.y, 0, characterAfterCaret.size.height); + } else { + return CGRectMake(characterAfterCaret.origin.x, characterAfterCaret.origin.y, 0, + characterAfterCaret.size.height); + } + } else if (rects.count == 2 && affinity == UITextStorageDirectionForward) { + // There are characters before and after the caret, with forward direction affinity. + // It's better to use the character after the caret. + CGRect characterAfterCaret = rects[1].rect; + // Return a zero-width rectangle along the upstream edge of the character after the caret + // position. + if ([rects[1] isKindOfClass:[FlutterTextSelectionRect class]] && + ((FlutterTextSelectionRect*)rects[1]).isRTL) { + return CGRectMake(characterAfterCaret.origin.x + characterAfterCaret.size.width, + characterAfterCaret.origin.y, 0, characterAfterCaret.size.height); + } else { + return CGRectMake(characterAfterCaret.origin.x, characterAfterCaret.origin.y, 0, + characterAfterCaret.size.height); + } + } -- (CGRect)bounds { - return _isFloatingCursorActive ? kSpacePanBounds : super.bounds; + // Covers 2 remaining cases: + // 1. there are characters before and after the caret, with backward direction affinity. + // 2. there is only 1 character before the caret (caret is at the end of text). + // For both cases, return a zero-width rectangle along the downstream edge of the character + // before the caret position. + CGRect characterBeforeCaret = rects[0].rect; + if ([rects[0] isKindOfClass:[FlutterTextSelectionRect class]] && + ((FlutterTextSelectionRect*)rects[0]).isRTL) { + return CGRectMake(characterBeforeCaret.origin.x, characterBeforeCaret.origin.y, 0, + characterBeforeCaret.size.height); + } else { + return CGRectMake(characterBeforeCaret.origin.x + characterBeforeCaret.size.width, + characterBeforeCaret.origin.y, 0, characterBeforeCaret.size.height); + } } - (UITextPosition*)closestPositionToPoint:(CGPoint)point { diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm index 84827c4722e87..1b86c21a03162 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm @@ -1429,6 +1429,29 @@ - (void)testClosestPositionToPointWithinRange { 1U, ((FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).index); } +- (void)testClosestPositionToPointWithPartialSelectionRects { + FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin]; + [inputView setTextInputState:@{@"text" : @"COMPOSING"}]; + + [inputView setSelectionRects:@[ [FlutterTextSelectionRect + selectionRectWithRect:CGRectMake(0, 0, 100, 100) + position:0U] ]]; + // Asking with a position at the end of selection rects should give you the trailing edge of + // the last rect. + XCTAssertTrue(CGRectEqualToRect( + [inputView caretRectForPosition:[FlutterTextPosition + positionWithIndex:1 + affinity:UITextStorageDirectionForward]], + CGRectMake(100, 0, 0, 100))); + // Asking with a position beyond the end of selection rects should return CGRectZero without + // crashing. + XCTAssertTrue(CGRectEqualToRect( + [inputView caretRectForPosition:[FlutterTextPosition + positionWithIndex:2 + affinity:UITextStorageDirectionForward]], + CGRectZero)); +} + #pragma mark - Floating Cursor - Tests - (void)testFloatingCursorDoesNotThrow {