From 058ffbe73fd6487365c1eff57bd72235f753055c Mon Sep 17 00:00:00 2001 From: hellohuanlin <41930132+hellohuanlin@users.noreply.github.com> Date: Wed, 15 Nov 2023 11:37:22 -0800 Subject: [PATCH] [ios]fix ios 16 auto correction highlight showing on top left corner (flutter/engine#47279) This PR hides the system highlights in iOS 16 (except when scribble is enabled). Note that auto correction highlight is still drawn by flutter, so it still works. I don't think we need to CP this, since it's iOS 16 only, and iOS 17 is already out. ## Why not use system highlight? Unlike iOS 17, the iOS 16 system highlight only respect the width provided by `firstRectForRange`, but not the height. I have audited all sizing related APIs in UITextInput (doc [here](https://developer.apple.com/documentation/uikit/uitextinput#1653155)), specifically, `firstRect(for:)`, `caretRect(for:)` and `selectionRects(for:)`, and they all return the correct height. ## About scribble The initial implementation of `firstRectForRange` (that returns the first selection rect) was introduced for the scribble feature on iPad (code [here](https://github.com/flutter/engine/commit/1d3165a31c9ae0b69265ec3abb8b194c3a378f02#diff-4c7b102c0690b8ec5e2212b079f5d69fe3f816c84e47ce94bc7bc89312f39e40R1487-R1505)). It turns out that a non-zero rect is required for scribble's advanced feature to work (e.g. inserting a space with a vertical bar). So we can't apply this fix for scribble. *List which issues are fixed by this PR. You must list at least one issue.* Fixes https://github.com/flutter/flutter/issues/136802 *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 --- .../Source/FlutterTextInputPlugin.mm | 10 +++ .../Source/FlutterTextInputPluginTest.mm | 87 +++++++++++++------ 2 files changed, 69 insertions(+), 28 deletions(-) diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm index 500d5cd8f4a..7168515b1a0 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm @@ -1690,6 +1690,16 @@ static BOOL IsSelectionRectBoundaryCloserToPoint(CGPoint point, } } + // The iOS 16 system highlight does not repect the height returned by `firstRectForRange` + // API (unlike iOS 17). So we return CGRectZero to hide it (unless if scribble is enabled). + // To support scribble's advanced gestures (e.g. insert a space with a vertical bar), + // at least 1 character's width is required. + if (@available(iOS 17, *)) { + // No-op + } else if (![self isScribbleAvailable]) { + return CGRectZero; + } + NSUInteger first = start; if (end < start) { first = end; diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm index 108979c1a3a..faee42df1b4 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm @@ -1558,6 +1558,31 @@ FLUTTER_ASSERT_ARC [inputView firstRectForRange:range])); } +- (void)testFirstRectForRangeReturnsNoneZeroRectWhenScribbleIsEnabled { + FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin]; + [inputView setTextInputState:@{@"text" : @"COMPOSING"}]; + + FlutterTextInputView* mockInputView = OCMPartialMock(inputView); + OCMStub([mockInputView isScribbleAvailable]).andReturn(YES); + + [inputView setSelectionRects:@[ + [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:0U], + [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 0, 100, 100) position:1U], + [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 0, 100, 100) position:2U], + [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 0, 100, 100) position:3U], + ]]; + + FlutterTextRange* multiRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 3)]; + + if (@available(iOS 17, *)) { + XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100), + [inputView firstRectForRange:multiRectRange])); + } else { + XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100), + [inputView firstRectForRange:multiRectRange])); + } +} + - (void)testFirstRectForRangeReturnsCorrectRectOnASingleLineLeftToRight { FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin]; [inputView setTextInputState:@{@"text" : @"COMPOSING"}]; @@ -1569,8 +1594,12 @@ FLUTTER_ASSERT_ARC [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 0, 100, 100) position:3U], ]]; FlutterTextRange* singleRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 1)]; - XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100), - [inputView firstRectForRange:singleRectRange])); + if (@available(iOS 17, *)) { + XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100), + [inputView firstRectForRange:singleRectRange])); + } else { + XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange])); + } FlutterTextRange* multiRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 3)]; @@ -1578,8 +1607,7 @@ FLUTTER_ASSERT_ARC XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100), [inputView firstRectForRange:multiRectRange])); } else { - XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100), - [inputView firstRectForRange:multiRectRange])); + XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange])); } [inputView setTextInputState:@{@"text" : @"COM"}]; @@ -1598,16 +1626,19 @@ FLUTTER_ASSERT_ARC [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:3U], ]]; FlutterTextRange* singleRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 1)]; - XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100), - [inputView firstRectForRange:singleRectRange])); + if (@available(iOS 17, *)) { + XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100), + [inputView firstRectForRange:singleRectRange])); + } else { + XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange])); + } FlutterTextRange* multiRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 3)]; if (@available(iOS 17, *)) { XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100), [inputView firstRectForRange:multiRectRange])); } else { - XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100), - [inputView firstRectForRange:multiRectRange])); + XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange])); } [inputView setTextInputState:@{@"text" : @"COM"}]; @@ -1630,8 +1661,12 @@ FLUTTER_ASSERT_ARC [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 100, 100, 100) position:7U], ]]; FlutterTextRange* singleRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 1)]; - XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100), - [inputView firstRectForRange:singleRectRange])); + if (@available(iOS 17, *)) { + XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100), + [inputView firstRectForRange:singleRectRange])); + } else { + XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange])); + } FlutterTextRange* multiRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 4)]; @@ -1639,8 +1674,7 @@ FLUTTER_ASSERT_ARC XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100), [inputView firstRectForRange:multiRectRange])); } else { - XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100), - [inputView firstRectForRange:multiRectRange])); + XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange])); } } @@ -1659,16 +1693,19 @@ FLUTTER_ASSERT_ARC [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 100, 100, 100) position:7U], ]]; FlutterTextRange* singleRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 1)]; - XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100), - [inputView firstRectForRange:singleRectRange])); + if (@available(iOS 17, *)) { + XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100), + [inputView firstRectForRange:singleRectRange])); + } else { + XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange])); + } FlutterTextRange* multiRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 4)]; if (@available(iOS 17, *)) { XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100), [inputView firstRectForRange:multiRectRange])); } else { - XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100), - [inputView firstRectForRange:multiRectRange])); + XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange])); } } @@ -1691,8 +1728,7 @@ FLUTTER_ASSERT_ARC XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, -10, 300, 120), [inputView firstRectForRange:multiRectRange])); } else { - XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 10, 100, 80), - [inputView firstRectForRange:multiRectRange])); + XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange])); } } @@ -1715,8 +1751,7 @@ FLUTTER_ASSERT_ARC XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, -10, 300, 120), [inputView firstRectForRange:multiRectRange])); } else { - XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, -10, 100, 120), - [inputView firstRectForRange:multiRectRange])); + XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange])); } } @@ -1739,8 +1774,7 @@ FLUTTER_ASSERT_ARC XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100), [inputView firstRectForRange:multiRectRange])); } else { - XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100), - [inputView firstRectForRange:multiRectRange])); + XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange])); } } @@ -1763,8 +1797,7 @@ FLUTTER_ASSERT_ARC XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100), [inputView firstRectForRange:multiRectRange])); } else { - XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100), - [inputView firstRectForRange:multiRectRange])); + XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange])); } } @@ -1787,8 +1820,7 @@ FLUTTER_ASSERT_ARC XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 400, 140), [inputView firstRectForRange:multiRectRange])); } else { - XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100), - [inputView firstRectForRange:multiRectRange])); + XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange])); } } @@ -1811,8 +1843,7 @@ FLUTTER_ASSERT_ARC XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 400, 140), [inputView firstRectForRange:multiRectRange])); } else { - XCTAssertTrue(CGRectEqualToRect(CGRectMake(300, 0, 100, 100), - [inputView firstRectForRange:multiRectRange])); + XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange])); } }