From 9ac2cb9518ff230e5ea9f1d152a66dc8d6ba88cd Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Fri, 17 Jul 2020 08:10:05 -0700 Subject: [PATCH] Fix loss of negative text selection ranges (flutter/engine#19785) iOS now matches Android behavior in that empty selections put the cursor at the start of the field. --- .../Source/FlutterTextInputPlugin.mm | 16 ++++++++++++++- .../Source/FlutterTextInputPluginTest.m | 20 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) 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 dc85fbee706..7130469ee7b 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 @@ -357,7 +357,21 @@ static NSString* uniqueIdFromDictionary(NSDictionary* dictionary) { selectedRange.length != oldSelectedRange.length) { needsEditingStateUpdate = YES; [self.inputDelegate selectionWillChange:self]; - [self setSelectedTextRangeLocal:[FlutterTextRange rangeWithNSRange:selectedRange]]; + + // The state may contain an invalid selection, such as when no selection was + // explicitly set in the framework. This is handled here by setting the + // selection to (0,0). In contrast, Android handles this situation by + // clearing the selection, but the result in both cases is that the cursor + // is placed at the beginning of the field. + bool selectionBaseIsValid = selectionBase > 0 && selectionBase <= ((NSInteger)self.text.length); + bool selectionExtentIsValid = + selectionExtent > 0 && selectionExtent <= ((NSInteger)self.text.length); + if (selectionBaseIsValid && selectionExtentIsValid) { + [self setSelectedTextRangeLocal:[FlutterTextRange rangeWithNSRange:selectedRange]]; + } else { + [self setSelectedTextRangeLocal:[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 0)]]; + } + _selectionAffinity = _kTextAffinityDownstream; if ([state[@"selectionAffinity"] isEqualToString:@(_kTextAffinityUpstream)]) _selectionAffinity = _kTextAffinityUpstream; diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m index 2a33beedbab..b2e6981d2b9 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m @@ -147,6 +147,26 @@ FLUTTER_ASSERT_ARC OCMReject([engine updateEditingClient:0 withState:[OCMArg any]]); } +- (void)testUpdateEditingClientNegativeSelection { + FlutterTextInputView* inputView = [[FlutterTextInputView alloc] init]; + inputView.textInputDelegate = engine; + + [inputView.text setString:@"SELECTION"]; + inputView.markedTextRange = nil; + inputView.selectedTextRange = nil; + + [inputView setTextInputState:@{ + @"text" : @"SELECTION", + @"selectionBase" : @-1, + @"selectionExtent" : @-1 + }]; + OCMVerify([engine updateEditingClient:0 + withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) { + return ([state[@"selectionBase"] intValue]) == 0 && + ([state[@"selectionExtent"] intValue] == 0); + }]]); +} + - (void)testAutofillInputViews { NSDictionary* template = @{ @"inputType" : @{@"name" : @"TextInuptType.text"},