diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h index 7ce919d22e0b8..39a98d319aff1 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h @@ -29,4 +29,5 @@ // Private methods made visible for testing @interface FlutterTextInputPlugin (TestMethods) - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result; +- (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange; @end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm index 0a948d550117c..75fba93c4caa5 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm @@ -21,6 +21,8 @@ static NSString* const kHideMethod = @"TextInput.hide"; static NSString* const kClearClientMethod = @"TextInput.clearClient"; static NSString* const kSetEditingStateMethod = @"TextInput.setEditingState"; +static NSString* const kSetEditableSizeAndTransform = @"TextInput.setEditableSizeAndTransform"; +static NSString* const kSetCaretRect = @"TextInput.setCaretRect"; static NSString* const kUpdateEditStateResponseMethod = @"TextInputClient.updateEditingState"; static NSString* const kPerformAction = @"TextInputClient.performAction"; static NSString* const kMultilineInputType = @"TextInputType.multiline"; @@ -39,6 +41,7 @@ static NSString* const kComposingBaseKey = @"composingBase"; static NSString* const kComposingExtentKey = @"composingExtent"; static NSString* const kTextKey = @"text"; +static NSString* const kTransformKey = @"transform"; /** * The affinity of the current cursor position. If the cursor is at a position representing @@ -163,6 +166,8 @@ - (instancetype)initWithViewController:(FlutterViewController*)viewController { _textInputContext = [[NSTextInputContext alloc] initWithClient:self]; _previouslyPressedFlags = 0; + _flutterViewController = viewController; + // Initialize with the zero matrix which is not // an affine transform. _editableTransform = CATransform3D(); @@ -215,10 +220,10 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { // engine since it sent this update, and needs to now be made to match the // engine's version of the state. [self updateEditState]; - } else if ([method isEqualToString:@"TextInput.setEditableSizeAndTransform"]) { + } else if ([method isEqualToString:kSetEditableSizeAndTransform]) { NSDictionary* state = call.arguments; - [self setEditableTransform:state[@"transform"]]; - } else if ([method isEqualToString:@"TextInput.setCaretRect"]) { + [self setEditableTransform:state[kTransformKey]]; + } else if ([method isEqualToString:kSetCaretRect]) { NSDictionary* rect = call.arguments; [self updateCaretRect:rect]; } else { @@ -462,9 +467,10 @@ - (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer) CGRect rect = CGRectApplyAffineTransform(_caretRect, CATransform3DGetAffineTransform(_editableTransform)); - // flip and convert to screen coordinates - double viewHeight = self.flutterViewController.view.bounds.size.height; - rect.origin.y = viewHeight - rect.origin.y; + // convert to window coordinates + rect = [self.flutterViewController.view convertRect:rect toView:nil]; + + // convert to screen coordinates return [self.flutterViewController.view.window convertRectToScreen:rect]; } else { return CGRectZero; diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm index 5712b1615b60f..0eeb2d6bc7d72 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm @@ -81,6 +81,80 @@ - (bool)testEmptyCompositionRange { return true; } +- (bool)testFirstRectForCharacterRange { + id engineMock = OCMClassMock([FlutterEngine class]); + id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); + OCMStub( // NOLINT(google-objc-avoid-throwing-exception) + [engineMock binaryMessenger]) + .andReturn(binaryMessengerMock); + id controllerMock = OCMClassMock([FlutterViewController class]); + OCMStub( // NOLINT(google-objc-avoid-throwing-exception) + [controllerMock engine]) + .andReturn(engineMock); + + id viewMock = OCMClassMock([NSView class]); + OCMStub( // NOLINT(google-objc-avoid-throwing-exception) + [viewMock bounds]) + .andReturn(NSMakeRect(0, 0, 200, 200)); + OCMStub( // NOLINT(google-objc-avoid-throwing-exception) + [controllerMock view]) + .andReturn(viewMock); + + id windowMock = OCMClassMock([NSWindow class]); + OCMStub( // NOLINT(google-objc-avoid-throwing-exception) + [viewMock window]) + .andReturn(windowMock); + + OCMExpect( // NOLINT(google-objc-avoid-throwing-exception) + [viewMock convertRect:NSMakeRect(28, 10, 2, 19) toView:nil]) + .andReturn(NSMakeRect(28, 10, 2, 19)); + + OCMExpect( // NOLINT(google-objc-avoid-throwing-exception) + [windowMock convertRectToScreen:NSMakeRect(28, 10, 2, 19)]) + .andReturn(NSMakeRect(38, 20, 2, 19)); + + FlutterTextInputPlugin* plugin = + [[FlutterTextInputPlugin alloc] initWithViewController:controllerMock]; + + FlutterMethodCall* call = [FlutterMethodCall + methodCallWithMethodName:@"TextInput.setEditableSizeAndTransform" + arguments:@{ + @"height" : @(20.0), + @"transform" : @[ + @(1.0), @(0.0), @(0.0), @(0.0), @(0.0), @(1.0), @(0.0), @(0.0), @(0.0), + @(0.0), @(1.0), @(0.0), @(20.0), @(10.0), @(0.0), @(1.0) + ], + @"width" : @(400.0), + }]; + + [plugin handleMethodCall:call + result:^(id){ + }]; + + call = [FlutterMethodCall methodCallWithMethodName:@"TextInput.setCaretRect" + arguments:@{ + @"height" : @(19.0), + @"width" : @(2.0), + @"x" : @(8.0), + @"y" : @(0.0), + }]; + + [plugin handleMethodCall:call + result:^(id){ + }]; + + NSRect rect = [plugin firstRectForCharacterRange:NSMakeRange(0, 0) actualRange:nullptr]; + + @try { + OCMVerify( // NOLINT(google-objc-avoid-throwing-exception) + [windowMock convertRectToScreen:NSMakeRect(28, 10, 2, 19)]); + } @catch (...) { + return false; + } + + return NSEqualRects(rect, NSMakeRect(38, 20, 2, 19)); +} + @end namespace flutter::testing { @@ -89,4 +163,8 @@ - (bool)testEmptyCompositionRange { ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testEmptyCompositionRange]); } +TEST(FlutterTextInputPluginTest, TestFirstRectForCharacterRange) { + ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testFirstRectForCharacterRange]); +} + } // namespace flutter::testing