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

[CP][ios][iOS 17]fix highlight on top left corner #44702

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -164,4 +164,16 @@ FLUTTER_DARWIN_EXPORT
- (instancetype)initWithOwner:(FlutterTextInputPlugin*)textInputPlugin NS_DESIGNATED_INITIALIZER;

@end

@interface UIViewController (FlutterScreenAndSceneIfLoaded)

/// Returns a UIWindowScene if the UIViewController's view is loaded, and nil otherwise.
- (UIWindowScene*)flutterWindowSceneIfViewLoaded API_AVAILABLE(ios(13.0));

/// Before iOS 13, returns the main screen; After iOS 13, returns the screen the UIViewController is
/// attached to if its view is loaded, and nil otherwise.
- (UIScreen*)flutterScreenIfViewLoaded;

@end

#endif // SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTERTEXTINPUTPLUGIN_H_
Original file line number Diff line number Diff line change
Expand Up @@ -1606,10 +1606,13 @@ - (CGRect)firstRectForRange:(UITextRange*)range {
NSUInteger start = ((FlutterTextPosition*)range.start).index;
NSUInteger end = ((FlutterTextPosition*)range.end).index;
if (_markedTextRange != nil) {
UIView* hostView = _textInputPlugin.hostView;
NSAssert(hostView == nil || [self isDescendantOfView:hostView], @"%@ is not a descendant of %@",
self, hostView);
// The candidates view can't be shown if the framework has not sent the
// first caret rect.
if (CGRectEqualToRect(kInvalidFirstRect, _markedRect)) {
return kInvalidFirstRect;
return hostView ? [hostView convertRect:kInvalidFirstRect toView:self] : kInvalidFirstRect;
}

if (CGRectEqualToRect(_cachedFirstRect, kInvalidFirstRect)) {
Expand All @@ -1623,9 +1626,6 @@ - (CGRect)firstRectForRange:(UITextRange*)range {
_cachedFirstRect = [self localRectFromFrameworkTransform:rect];
}

UIView* hostView = _textInputPlugin.hostView;
NSAssert(hostView == nil || [self isDescendantOfView:hostView], @"%@ is not a descendant of %@",
self, hostView);
return hostView ? [hostView convertRect:_cachedFirstRect toView:self] : _cachedFirstRect;
}

Expand Down Expand Up @@ -2301,6 +2301,16 @@ - (void)setEditableSizeAndTransform:(NSDictionary*)dictionary {
_activeView.frame =
CGRectMake(0, 0, [dictionary[@"width"] intValue], [dictionary[@"height"] intValue]);
_activeView.tintColor = [UIColor clearColor];
} else {
// View must be loaded at this point.
UIScreen* screen = _viewController.flutterScreenIfViewLoaded;

// Position FlutterTextInputView outside of the screen (if scribble is disabled).
// This is to fix a bug where native auto-correction highlight is displayed on
// top left corner of the screen (See: https://github.com/flutter/flutter/issues/131695)
// and a bug where the native auto-correction suggestion menu displayed (See:
// https://github.com/flutter/flutter/issues/130818).
_inputHider.frame = CGRectMake(0, -screen.bounds.size.height, 0, 0);
}
}

Expand Down Expand Up @@ -2752,3 +2762,26 @@ - (BOOL)handlePress:(nonnull FlutterUIPressProxy*)press API_AVAILABLE(ios(13.4))
return NO;
}
@end

@implementation UIViewController (FlutterScreenAndSceneIfLoaded)

- (UIWindowScene*)flutterWindowSceneIfViewLoaded {
if (self.viewIfLoaded == nil) {
FML_LOG(WARNING) << "Trying to access the window scene before the view is loaded.";
return nil;
}
return self.viewIfLoaded.window.windowScene;
}

- (UIScreen*)flutterScreenIfViewLoaded {
if (@available(iOS 13.0, *)) {
if (self.viewIfLoaded == nil) {
FML_LOG(WARNING) << "Trying to access the screen before the view is loaded.";
return nil;
}
return [self flutterWindowSceneIfViewLoaded].screen;
}
return UIScreen.mainScreen;
}

@end
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ @interface FlutterSecureTextInputView : FlutterTextInputView

@interface FlutterTextInputPlugin ()
@property(nonatomic, assign) FlutterTextInputView* activeView;
@property(nonatomic, readonly) UIView* inputHider;
@property(nonatomic, readonly)
NSMutableDictionary<NSString*, FlutterTextInputView*>* autofillContext;

Expand Down Expand Up @@ -1321,44 +1322,53 @@ - (void)testUpdateFirstRectForRange {
@(-6.0), @(3.0), @(9.0), @(1.0)
];

CGRect kInvalidFirstRectRelative =
[textInputPlugin.viewController.view convertRect:kInvalidFirstRect toView:inputView];

// Invalid since we don't have the transform or the rect.
XCTAssertTrue(CGRectEqualToRect(kInvalidFirstRect, [inputView firstRectForRange:range]));
XCTAssertTrue(CGRectEqualToRect(kInvalidFirstRectRelative, [inputView firstRectForRange:range]));

[inputView setEditableTransform:yOffsetMatrix];
// Invalid since we don't have the rect.
XCTAssertTrue(CGRectEqualToRect(kInvalidFirstRect, [inputView firstRectForRange:range]));
XCTAssertTrue(CGRectEqualToRect(kInvalidFirstRectRelative, [inputView firstRectForRange:range]));

// Valid rect and transform.
CGRect testRect = CGRectMake(0, 0, 100, 100);
[inputView setMarkedRect:testRect];

CGRect finalRect = CGRectOffset(testRect, 0, 200);
XCTAssertTrue(CGRectEqualToRect(finalRect, [inputView firstRectForRange:range]));
CGRect finalRectRelative = [textInputPlugin.viewController.view convertRect:finalRect
toView:inputView];
XCTAssertTrue(CGRectEqualToRect(finalRectRelative, [inputView firstRectForRange:range]));
// Idempotent.
XCTAssertTrue(CGRectEqualToRect(finalRect, [inputView firstRectForRange:range]));
XCTAssertTrue(CGRectEqualToRect(finalRectRelative, [inputView firstRectForRange:range]));

// Use an invalid matrix:
[inputView setEditableTransform:zeroMatrix];
// Invalid matrix is invalid.
XCTAssertTrue(CGRectEqualToRect(kInvalidFirstRect, [inputView firstRectForRange:range]));
XCTAssertTrue(CGRectEqualToRect(kInvalidFirstRect, [inputView firstRectForRange:range]));
XCTAssertTrue(CGRectEqualToRect(kInvalidFirstRectRelative, [inputView firstRectForRange:range]));
XCTAssertTrue(CGRectEqualToRect(kInvalidFirstRectRelative, [inputView firstRectForRange:range]));

// Revert the invalid matrix change.
[inputView setEditableTransform:yOffsetMatrix];
[inputView setMarkedRect:testRect];
XCTAssertTrue(CGRectEqualToRect(finalRect, [inputView firstRectForRange:range]));
XCTAssertTrue(CGRectEqualToRect(finalRectRelative, [inputView firstRectForRange:range]));

// Use an invalid rect:
[inputView setMarkedRect:kInvalidFirstRect];
// Invalid marked rect is invalid.
XCTAssertTrue(CGRectEqualToRect(kInvalidFirstRect, [inputView firstRectForRange:range]));
XCTAssertTrue(CGRectEqualToRect(kInvalidFirstRect, [inputView firstRectForRange:range]));
XCTAssertTrue(CGRectEqualToRect(kInvalidFirstRectRelative, [inputView firstRectForRange:range]));
XCTAssertTrue(CGRectEqualToRect(kInvalidFirstRectRelative, [inputView firstRectForRange:range]));

// Use a 3d affine transform that does 3d-scaling, z-index rotating and 3d translation.
[inputView setEditableTransform:affineMatrix];
[inputView setMarkedRect:testRect];
XCTAssertTrue(
CGRectEqualToRect(CGRectMake(-306, 3, 300, 300), [inputView firstRectForRange:range]));

CGRect relativeRect =
[textInputPlugin.viewController.view convertRect:CGRectMake(-306, 3, 300, 300)
toView:inputView];

XCTAssertTrue(CGRectEqualToRect(relativeRect, [inputView firstRectForRange:range]));

NSAssert(inputView.superview, @"inputView is not in the view hierarchy!");
const CGPoint offset = CGPointMake(113, 119);
Expand All @@ -1367,8 +1377,8 @@ - (void)testUpdateFirstRectForRange {
inputView.frame = currentFrame;
// Moving the input view within the FlutterView shouldn't affect the coordinates,
// since the framework sends us global coordinates.
XCTAssertTrue(CGRectEqualToRect(CGRectMake(-306 - 113, 3 - 119, 300, 300),
[inputView firstRectForRange:range]));
CGRect target = CGRectOffset(relativeRect, -113, -119);
XCTAssertTrue(CGRectEqualToRect(target, [inputView firstRectForRange:range]));
}

- (void)testFirstRectForRangeReturnsCorrectSelectionRect {
Expand Down Expand Up @@ -2190,6 +2200,76 @@ - (void)testInitialActiveViewCantAccessTextInputDelegate {
XCTAssertNil(textInputPlugin.activeView.textInputDelegate);
}

- (void)testInputHiderIsOffScreenWhenScribbleIsDisabled {
FlutterTextInputPlugin* myInputPlugin =
[[FlutterTextInputPlugin alloc] initWithDelegate:OCMClassMock([FlutterEngine class])];
myInputPlugin.viewController = viewController;

NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
XCTAssertEqual(scenes.count, 1UL, @"There must only be 1 scene for test");
UIScene* scene = scenes.anyObject;
XCTAssert([scene isKindOfClass:[UIWindowScene class]], @"Must be a window scene for test");
UIWindowScene* windowScene = (UIWindowScene*)scene;
XCTAssert(windowScene.windows.count > 0, @"There must be at least 1 window for test");
UIWindow* window = windowScene.windows[0];
[window addSubview:viewController.view];
[viewController loadView];
UIScreen* screen = viewController.flutterScreenIfViewLoaded;
XCTAssertNotNil(screen, @"Screen must be present at this point");

FlutterMethodCall* setClientCall =
[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
arguments:@[ @(123), self.mutableTemplateCopy ]];
[myInputPlugin handleMethodCall:setClientCall
result:^(id _Nullable result){
}];

FlutterTextInputView* mockInputView = OCMPartialMock(myInputPlugin.activeView);
OCMStub([mockInputView isScribbleAvailable]).andReturn(NO);

// yOffset = 200.
NSArray* yOffsetMatrix = @[ @1, @0, @0, @0, @0, @1, @0, @0, @0, @0, @1, @0, @0, @200, @0, @1 ];

FlutterMethodCall* setPlatformViewClientCall =
[FlutterMethodCall methodCallWithMethodName:@"TextInput.setEditableSizeAndTransform"
arguments:@{@"transform" : yOffsetMatrix}];
[myInputPlugin handleMethodCall:setPlatformViewClientCall
result:^(id _Nullable result){
}];

CGRect offScreenRect = CGRectMake(0, -screen.bounds.size.height, 0, 0);
XCTAssert(CGRectEqualToRect(myInputPlugin.inputHider.frame, offScreenRect),
@"The input hider should stay offScreen if scribble is disabled.");
}

- (void)testInputHiderIsOnScreenWhenScribbleIsEnabled {
FlutterTextInputPlugin* myInputPlugin =
[[FlutterTextInputPlugin alloc] initWithDelegate:OCMClassMock([FlutterEngine class])];

FlutterMethodCall* setClientCall =
[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
arguments:@[ @(123), self.mutableTemplateCopy ]];
[myInputPlugin handleMethodCall:setClientCall
result:^(id _Nullable result){
}];

FlutterTextInputView* mockInputView = OCMPartialMock(myInputPlugin.activeView);
OCMStub([mockInputView isScribbleAvailable]).andReturn(YES);

// yOffset = 200.
NSArray* yOffsetMatrix = @[ @1, @0, @0, @0, @0, @1, @0, @0, @0, @0, @1, @0, @0, @200, @0, @1 ];

FlutterMethodCall* setPlatformViewClientCall =
[FlutterMethodCall methodCallWithMethodName:@"TextInput.setEditableSizeAndTransform"
arguments:@{@"transform" : yOffsetMatrix}];
[myInputPlugin handleMethodCall:setPlatformViewClientCall
result:^(id _Nullable result){
}];

XCTAssertEqual(myInputPlugin.inputHider.frame.origin.y, 200,
@"The input hider should be brought on screen if scribble is enabled");
}

#pragma mark - Accessibility - Tests

- (void)testUITextInputAccessibilityNotHiddenWhenShowed {
Expand Down