8 #import <Foundation/Foundation.h>
9 #import <UIKit/UIKit.h>
11 #include "unicode/uchar.h"
13 #include "flutter/fml/logging.h"
14 #include "flutter/fml/platform/darwin/string_range_sanitization.h"
38 #pragma mark - TextInput channel method names.
47 @"TextInput.setEditableSizeAndTransform";
58 @"TextInput.onPointerMoveForInteractiveKeyboard";
60 @"TextInput.onPointerUpForInteractiveKeyboard";
62 #pragma mark - TextInputConfiguration Field Names
83 #pragma mark - Static Functions
86 static BOOL
IsEmoji(NSString* text, NSRange charRange) {
88 BOOL gotCodePoint = [text getBytes:&codePoint
89 maxLength:
sizeof(codePoint)
91 encoding:NSUTF32StringEncoding
95 return gotCodePoint && u_hasBinaryProperty(codePoint, UCHAR_EMOJI);
103 NSString* inputType = type[
@"name"];
104 return ![inputType isEqualToString:
@"TextInputType.none"];
107 NSString* inputType = type[
@"name"];
108 if ([inputType isEqualToString:
@"TextInputType.address"]) {
109 return UIKeyboardTypeDefault;
111 if ([inputType isEqualToString:
@"TextInputType.datetime"]) {
112 return UIKeyboardTypeNumbersAndPunctuation;
114 if ([inputType isEqualToString:
@"TextInputType.emailAddress"]) {
115 return UIKeyboardTypeEmailAddress;
117 if ([inputType isEqualToString:
@"TextInputType.multiline"]) {
118 return UIKeyboardTypeDefault;
120 if ([inputType isEqualToString:
@"TextInputType.name"]) {
121 return UIKeyboardTypeNamePhonePad;
123 if ([inputType isEqualToString:
@"TextInputType.number"]) {
124 if ([type[
@"signed"] boolValue]) {
125 return UIKeyboardTypeNumbersAndPunctuation;
127 if ([type[
@"decimal"] boolValue]) {
128 return UIKeyboardTypeDecimalPad;
130 return UIKeyboardTypeNumberPad;
132 if ([inputType isEqualToString:
@"TextInputType.phone"]) {
133 return UIKeyboardTypePhonePad;
135 if ([inputType isEqualToString:
@"TextInputType.text"]) {
136 return UIKeyboardTypeDefault;
138 if ([inputType isEqualToString:
@"TextInputType.url"]) {
139 return UIKeyboardTypeURL;
141 if ([inputType isEqualToString:
@"TextInputType.visiblePassword"]) {
142 return UIKeyboardTypeASCIICapable;
144 if ([inputType isEqualToString:
@"TextInputType.webSearch"]) {
145 return UIKeyboardTypeWebSearch;
147 if ([inputType isEqualToString:
@"TextInputType.twitter"]) {
148 return UIKeyboardTypeTwitter;
150 return UIKeyboardTypeDefault;
154 NSString* textCapitalization = type[
@"textCapitalization"];
155 if ([textCapitalization isEqualToString:
@"TextCapitalization.characters"]) {
156 return UITextAutocapitalizationTypeAllCharacters;
157 }
else if ([textCapitalization isEqualToString:
@"TextCapitalization.sentences"]) {
158 return UITextAutocapitalizationTypeSentences;
159 }
else if ([textCapitalization isEqualToString:
@"TextCapitalization.words"]) {
160 return UITextAutocapitalizationTypeWords;
162 return UITextAutocapitalizationTypeNone;
170 if ([inputType isEqualToString:
@"TextInputAction.unspecified"]) {
171 return UIReturnKeyDefault;
174 if ([inputType isEqualToString:
@"TextInputAction.done"]) {
175 return UIReturnKeyDone;
178 if ([inputType isEqualToString:
@"TextInputAction.go"]) {
179 return UIReturnKeyGo;
182 if ([inputType isEqualToString:
@"TextInputAction.send"]) {
183 return UIReturnKeySend;
186 if ([inputType isEqualToString:
@"TextInputAction.search"]) {
187 return UIReturnKeySearch;
190 if ([inputType isEqualToString:
@"TextInputAction.next"]) {
191 return UIReturnKeyNext;
194 if ([inputType isEqualToString:
@"TextInputAction.continueAction"]) {
195 return UIReturnKeyContinue;
198 if ([inputType isEqualToString:
@"TextInputAction.join"]) {
199 return UIReturnKeyJoin;
202 if ([inputType isEqualToString:
@"TextInputAction.route"]) {
203 return UIReturnKeyRoute;
206 if ([inputType isEqualToString:
@"TextInputAction.emergencyCall"]) {
207 return UIReturnKeyEmergencyCall;
210 if ([inputType isEqualToString:
@"TextInputAction.newline"]) {
211 return UIReturnKeyDefault;
215 return UIReturnKeyDefault;
219 if (!hints || hints.count == 0) {
224 NSString* hint = hints[0];
225 if ([hint isEqualToString:
@"addressCityAndState"]) {
226 return UITextContentTypeAddressCityAndState;
229 if ([hint isEqualToString:
@"addressState"]) {
230 return UITextContentTypeAddressState;
233 if ([hint isEqualToString:
@"addressCity"]) {
234 return UITextContentTypeAddressCity;
237 if ([hint isEqualToString:
@"sublocality"]) {
238 return UITextContentTypeSublocality;
241 if ([hint isEqualToString:
@"streetAddressLine1"]) {
242 return UITextContentTypeStreetAddressLine1;
245 if ([hint isEqualToString:
@"streetAddressLine2"]) {
246 return UITextContentTypeStreetAddressLine2;
249 if ([hint isEqualToString:
@"countryName"]) {
250 return UITextContentTypeCountryName;
253 if ([hint isEqualToString:
@"fullStreetAddress"]) {
254 return UITextContentTypeFullStreetAddress;
257 if ([hint isEqualToString:
@"postalCode"]) {
258 return UITextContentTypePostalCode;
261 if ([hint isEqualToString:
@"location"]) {
262 return UITextContentTypeLocation;
265 if ([hint isEqualToString:
@"creditCardNumber"]) {
266 return UITextContentTypeCreditCardNumber;
269 if ([hint isEqualToString:
@"email"]) {
270 return UITextContentTypeEmailAddress;
273 if ([hint isEqualToString:
@"jobTitle"]) {
274 return UITextContentTypeJobTitle;
277 if ([hint isEqualToString:
@"givenName"]) {
278 return UITextContentTypeGivenName;
281 if ([hint isEqualToString:
@"middleName"]) {
282 return UITextContentTypeMiddleName;
285 if ([hint isEqualToString:
@"familyName"]) {
286 return UITextContentTypeFamilyName;
289 if ([hint isEqualToString:
@"name"]) {
290 return UITextContentTypeName;
293 if ([hint isEqualToString:
@"namePrefix"]) {
294 return UITextContentTypeNamePrefix;
297 if ([hint isEqualToString:
@"nameSuffix"]) {
298 return UITextContentTypeNameSuffix;
301 if ([hint isEqualToString:
@"nickname"]) {
302 return UITextContentTypeNickname;
305 if ([hint isEqualToString:
@"organizationName"]) {
306 return UITextContentTypeOrganizationName;
309 if ([hint isEqualToString:
@"telephoneNumber"]) {
310 return UITextContentTypeTelephoneNumber;
313 if ([hint isEqualToString:
@"password"]) {
314 return UITextContentTypePassword;
317 if ([hint isEqualToString:
@"oneTimeCode"]) {
318 return UITextContentTypeOneTimeCode;
321 if ([hint isEqualToString:
@"newPassword"]) {
322 return UITextContentTypeNewPassword;
390 typedef NS_ENUM(NSInteger, FlutterAutofillType) {
394 kFlutterAutofillTypeNone,
395 kFlutterAutofillTypeRegular,
396 kFlutterAutofillTypePassword,
406 if (isSecureTextEntry) {
413 if ([contentType isEqualToString:UITextContentTypePassword] ||
414 [contentType isEqualToString:UITextContentTypeUsername]) {
418 if ([contentType isEqualToString:UITextContentTypeNewPassword]) {
428 return kFlutterAutofillTypePassword;
433 return kFlutterAutofillTypePassword;
438 return !autofill || [contentType isEqualToString:
@""] ? kFlutterAutofillTypeNone
439 : kFlutterAutofillTypeRegular;
443 return fabsf(x - y) <= delta;
469 CGRect selectionRect,
470 BOOL selectionRectIsRTL,
471 BOOL useTrailingBoundaryOfSelectionRect,
472 CGRect otherSelectionRect,
473 BOOL otherSelectionRectIsRTL,
474 CGFloat verticalPrecision) {
476 if (CGRectContainsPoint(
478 selectionRect.origin.x + ((useTrailingBoundaryOfSelectionRect ^ selectionRectIsRTL)
479 ? 0.5 * selectionRect.size.width
481 selectionRect.origin.y, 0.5 * selectionRect.size.width, selectionRect.size.height),
486 CGPoint pointForSelectionRect = CGPointMake(
487 selectionRect.origin.x +
488 (selectionRectIsRTL ^ useTrailingBoundaryOfSelectionRect ? selectionRect.size.width : 0),
489 selectionRect.origin.y + selectionRect.size.height * 0.5);
490 float yDist = fabs(pointForSelectionRect.y - point.y);
491 float xDist = fabs(pointForSelectionRect.x - point.x);
494 CGPoint pointForOtherSelectionRect = CGPointMake(
495 otherSelectionRect.origin.x + (otherSelectionRectIsRTL ? otherSelectionRect.size.width : 0),
496 otherSelectionRect.origin.y + otherSelectionRect.size.height * 0.5);
497 float yDistOther = fabs(pointForOtherSelectionRect.y - point.y);
498 float xDistOther = fabs(pointForOtherSelectionRect.x - point.x);
503 BOOL isCloserVertically = yDist < yDistOther - verticalPrecision;
505 BOOL isAboveBottomOfLine = point.y <= selectionRect.origin.y + selectionRect.size.height;
506 BOOL isCloserHorizontally = xDist < xDistOther;
507 BOOL isBelowBottomOfLine = point.y > selectionRect.origin.y + selectionRect.size.height;
510 if (selectionRectIsRTL) {
511 isFarther = selectionRect.origin.x < otherSelectionRect.origin.x;
513 isFarther = selectionRect.origin.x +
514 (useTrailingBoundaryOfSelectionRect ? selectionRect.size.width : 0) >
515 otherSelectionRect.origin.x;
517 return (isCloserVertically ||
518 (isEqualVertically &&
519 ((isAboveBottomOfLine && isCloserHorizontally) || (isBelowBottomOfLine && isFarther))));
522 #pragma mark - FlutterTextPosition
526 + (instancetype)positionWithIndex:(NSUInteger)index {
527 return [[
FlutterTextPosition alloc] initWithIndex:index affinity:UITextStorageDirectionForward];
530 + (instancetype)positionWithIndex:(NSUInteger)index affinity:(UITextStorageDirection)affinity {
534 - (instancetype)initWithIndex:(NSUInteger)index affinity:(UITextStorageDirection)affinity {
545 #pragma mark - FlutterTextRange
549 + (instancetype)rangeWithNSRange:(NSRange)range {
553 - (instancetype)initWithNSRange:(NSRange)range {
561 - (UITextPosition*)start {
563 affinity:UITextStorageDirectionForward];
566 - (UITextPosition*)end {
568 affinity:UITextStorageDirectionBackward];
572 return self.range.length == 0;
575 - (id)copyWithZone:(NSZone*)zone {
580 return NSEqualRanges(
self.
range, other.
range);
584 #pragma mark - FlutterTokenizer
594 - (instancetype)initWithTextInput:(UIResponder<UITextInput>*)textInput {
596 @"The FlutterTokenizer can only be used in a FlutterTextInputView");
597 self = [
super initWithTextInput:textInput];
604 - (UITextRange*)rangeEnclosingPosition:(UITextPosition*)position
605 withGranularity:(UITextGranularity)granularity
606 inDirection:(UITextDirection)direction {
608 switch (granularity) {
609 case UITextGranularityLine:
612 result = [
self lineEnclosingPosition:position inDirection:direction];
614 case UITextGranularityCharacter:
615 case UITextGranularityWord:
616 case UITextGranularitySentence:
617 case UITextGranularityParagraph:
618 case UITextGranularityDocument:
620 result = [
super rangeEnclosingPosition:position
621 withGranularity:granularity
622 inDirection:direction];
628 - (UITextRange*)lineEnclosingPosition:(UITextPosition*)position
629 inDirection:(UITextDirection)direction {
631 if (@available(iOS 17.0, *)) {
636 if (flutterPosition.
index > _textInputView.text.length ||
637 (flutterPosition.
index == _textInputView.text.length &&
638 direction == UITextStorageDirectionForward)) {
644 NSString* textAfter = [_textInputView
645 textInRange:[_textInputView textRangeFromPosition:position
646 toPosition:[_textInputView endOfDocument]]];
647 NSArray<NSString*>* linesAfter = [textAfter componentsSeparatedByString:@"\n"];
648 NSInteger offSetToLineBreak = [linesAfter firstObject].length;
649 UITextPosition* lineBreakAfter = [_textInputView positionFromPosition:position
650 offset:offSetToLineBreak];
652 NSString* textBefore = [_textInputView
653 textInRange:[_textInputView textRangeFromPosition:[_textInputView beginningOfDocument]
654 toPosition:position]];
655 NSArray<NSString*>* linesBefore = [textBefore componentsSeparatedByString:@"\n"];
656 NSInteger offSetFromLineBreak = [linesBefore lastObject].length;
657 UITextPosition* lineBreakBefore = [_textInputView positionFromPosition:position
658 offset:-offSetFromLineBreak];
660 return [_textInputView textRangeFromPosition:lineBreakBefore toPosition:lineBreakAfter];
665 #pragma mark - FlutterTextSelectionRect
670 @synthesize rect = _rect;
676 + (instancetype)selectionRectWithRectAndInfo:(CGRect)rect
677 position:(NSUInteger)position
678 writingDirection:(NSWritingDirection)writingDirection
679 containsStart:(BOOL)containsStart
680 containsEnd:(BOOL)containsEnd
681 isVertical:(BOOL)isVertical {
684 writingDirection:writingDirection
685 containsStart:containsStart
686 containsEnd:containsEnd
687 isVertical:isVertical];
690 + (instancetype)selectionRectWithRect:(CGRect)rect position:(NSUInteger)position {
693 writingDirection:NSWritingDirectionNatural
699 + (instancetype)selectionRectWithRect:(CGRect)rect
700 position:(NSUInteger)position
701 writingDirection:(NSWritingDirection)writingDirection {
704 writingDirection:writingDirection
710 - (instancetype)initWithRectAndInfo:(CGRect)rect
711 position:(NSUInteger)position
712 writingDirection:(NSWritingDirection)writingDirection
713 containsStart:(BOOL)containsStart
714 containsEnd:(BOOL)containsEnd
715 isVertical:(BOOL)isVertical {
729 return _writingDirection == NSWritingDirectionRightToLeft;
734 #pragma mark - FlutterTextPlaceholder
738 - (NSArray<UITextSelectionRect*>*)rects {
754 @property(nonatomic, retain, readonly) UITextField*
textField;
758 UITextField* _textField;
763 _textField = [[UITextField alloc] init];
768 - (BOOL)isKindOfClass:(Class)aClass {
769 return [
super isKindOfClass:aClass] || (aClass == [UITextField class]);
772 - (NSMethodSignature*)methodSignatureForSelector:(
SEL)aSelector {
773 NSMethodSignature* signature = [
super methodSignatureForSelector:aSelector];
775 signature = [
self.textField methodSignatureForSelector:aSelector];
780 - (void)forwardInvocation:(NSInvocation*)anInvocation {
781 [anInvocation invokeWithTarget:self.textField];
787 @property(nonatomic, readonly, weak) id<FlutterTextInputDelegate> textInputDelegate;
788 @property(nonatomic, readonly) UIView* hostView;
793 @property(nonatomic, copy) NSString* autofillId;
794 @property(nonatomic, readonly) CATransform3D editableTransform;
795 @property(nonatomic, assign) CGRect markedRect;
797 @property(nonatomic, assign) BOOL preventCursorDismissWhenResignFirstResponder;
798 @property(nonatomic) BOOL isVisibleToAutofill;
799 @property(nonatomic, assign) BOOL accessibilityEnabled;
800 @property(nonatomic, assign)
int textInputClient;
804 @property(nonatomic, copy) NSString* temporarilyDeletedComposedCharacter;
805 @property(nonatomic, assign) CGRect editMenuTargetRect;
806 @property(nonatomic, strong) NSArray<NSDictionary*>* editMenuItems;
808 - (void)setEditableTransform:(NSArray*)matrix;
812 int _textInputClient;
829 @synthesize tokenizer = _tokenizer;
832 self = [
super initWithFrame:CGRectZero];
835 _textInputClient = 0;
837 _preventCursorDismissWhenResignFirstResponder = NO;
840 _text = [[NSMutableString alloc] init];
845 _pendingDeltas = [[NSMutableArray alloc] init];
848 _editableTransform = CATransform3D();
851 _autocapitalizationType = UITextAutocapitalizationTypeSentences;
852 _autocorrectionType = UITextAutocorrectionTypeDefault;
853 _spellCheckingType = UITextSpellCheckingTypeDefault;
854 _enablesReturnKeyAutomatically = NO;
855 _keyboardAppearance = UIKeyboardAppearanceDefault;
856 _keyboardType = UIKeyboardTypeDefault;
857 _returnKeyType = UIReturnKeyDone;
858 _secureTextEntry = NO;
859 _enableDeltaModel = NO;
861 _accessibilityEnabled = NO;
862 _smartQuotesType = UITextSmartQuotesTypeYes;
863 _smartDashesType = UITextSmartDashesTypeYes;
864 _selectionRects = [[NSArray alloc] init];
866 if (@available(iOS 14.0, *)) {
867 UIScribbleInteraction* interaction = [[UIScribbleInteraction alloc] initWithDelegate:self];
868 [
self addInteraction:interaction];
872 if (@available(iOS 16.0, *)) {
873 _editMenuInteraction = [[UIEditMenuInteraction alloc] initWithDelegate:self];
874 [
self addInteraction:_editMenuInteraction];
880 - (void)handleSearchWebAction {
881 [
self.textInputDelegate flutterTextInputView:self
882 searchWebWithSelectedText:[
self textInRange:_selectedTextRange]];
885 - (void)handleLookUpAction {
886 [
self.textInputDelegate flutterTextInputView:self
887 lookUpSelectedText:[
self textInRange:_selectedTextRange]];
890 - (void)handleShareAction {
891 [
self.textInputDelegate flutterTextInputView:self
892 shareSelectedText:[
self textInRange:_selectedTextRange]];
896 - (UICommand*)searchCommandWithSelector:(
SEL)selector
897 element:(UIMenuElement*)element API_AVAILABLE(ios(16.0)) {
898 if ([element isKindOfClass:UICommand.class]) {
899 UICommand* command = (UICommand*)element;
900 return command.action == selector ? command : nil;
901 }
else if ([element isKindOfClass:UIMenu.class]) {
902 NSArray<UIMenuElement*>* children = ((UIMenu*)element).children;
903 for (UIMenuElement* child in children) {
904 UICommand* result = [
self searchCommandWithSelector:selector element:child];
915 - (void)addBasicEditingCommandToItems:(NSMutableArray*)items
917 selector:(
SEL)selector
918 suggestedMenu:(UIMenu*)suggestedMenu {
919 UICommand* command = [
self searchCommandWithSelector:selector element:suggestedMenu];
921 [items addObject:command];
923 FML_LOG(ERROR) <<
"Cannot find context menu item of type \"" << type.UTF8String <<
"\".";
927 - (void)addAdditionalBasicCommandToItems:(NSMutableArray*)items
929 selector:(
SEL)selector
930 encodedItem:(NSDictionary<NSString*,
id>*)encodedItem {
931 NSString* title = encodedItem[@"title"];
933 UICommand* command = [UICommand commandWithTitle:title
937 [items addObject:command];
939 FML_LOG(ERROR) <<
"Missing title for context menu item of type \"" << type.UTF8String <<
"\".";
943 - (UIMenu*)editMenuInteraction:(UIEditMenuInteraction*)interaction
944 menuForConfiguration:(UIEditMenuConfiguration*)configuration
945 suggestedActions:(NSArray<UIMenuElement*>*)suggestedActions API_AVAILABLE(ios(16.0)) {
946 UIMenu* suggestedMenu = [UIMenu menuWithChildren:suggestedActions];
947 if (!_editMenuItems) {
948 return suggestedMenu;
951 NSMutableArray* items = [NSMutableArray array];
952 for (NSDictionary<NSString*, id>* encodedItem in _editMenuItems) {
953 NSString* type = encodedItem[@"type"];
954 if ([type isEqualToString:
@"copy"]) {
955 [
self addBasicEditingCommandToItems:items
957 selector:@selector(copy:)
958 suggestedMenu:suggestedMenu];
959 }
else if ([type isEqualToString:
@"paste"]) {
960 [
self addBasicEditingCommandToItems:items
962 selector:@selector(paste:)
963 suggestedMenu:suggestedMenu];
964 }
else if ([type isEqualToString:
@"cut"]) {
965 [
self addBasicEditingCommandToItems:items
967 selector:@selector(cut:)
968 suggestedMenu:suggestedMenu];
969 }
else if ([type isEqualToString:
@"delete"]) {
970 [
self addBasicEditingCommandToItems:items
972 selector:@selector(delete:)
973 suggestedMenu:suggestedMenu];
974 }
else if ([type isEqualToString:
@"selectAll"]) {
975 [
self addBasicEditingCommandToItems:items
977 selector:@selector(selectAll:)
978 suggestedMenu:suggestedMenu];
979 }
else if ([type isEqualToString:
@"searchWeb"]) {
980 [
self addAdditionalBasicCommandToItems:items
982 selector:@selector(handleSearchWebAction)
983 encodedItem:encodedItem];
984 }
else if ([type isEqualToString:
@"share"]) {
985 [
self addAdditionalBasicCommandToItems:items
987 selector:@selector(handleShareAction)
988 encodedItem:encodedItem];
989 }
else if ([type isEqualToString:
@"lookUp"]) {
990 [
self addAdditionalBasicCommandToItems:items
992 selector:@selector(handleLookUpAction)
993 encodedItem:encodedItem];
996 return [UIMenu menuWithChildren:items];
999 - (void)editMenuInteraction:(UIEditMenuInteraction*)interaction
1000 willDismissMenuForConfiguration:(UIEditMenuConfiguration*)configuration
1001 animator:(
id<UIEditMenuInteractionAnimating>)animator
1002 API_AVAILABLE(ios(16.0)) {
1003 [
self.textInputDelegate flutterTextInputView:self
1004 willDismissEditMenuWithTextInputClient:_textInputClient];
1007 - (CGRect)editMenuInteraction:(UIEditMenuInteraction*)interaction
1008 targetRectForConfiguration:(UIEditMenuConfiguration*)configuration API_AVAILABLE(ios(16.0)) {
1009 return _editMenuTargetRect;
1012 - (void)showEditMenuWithTargetRect:(CGRect)targetRect
1013 items:(NSArray<NSDictionary*>*)items API_AVAILABLE(ios(16.0)) {
1014 _editMenuTargetRect = targetRect;
1015 _editMenuItems = items;
1016 UIEditMenuConfiguration* config =
1017 [UIEditMenuConfiguration configurationWithIdentifier:nil sourcePoint:CGPointZero];
1018 [
self.editMenuInteraction presentEditMenuWithConfiguration:config];
1022 [
self.editMenuInteraction dismissMenu];
1025 - (void)configureWithDictionary:(NSDictionary*)configuration {
1026 NSDictionary* inputType = configuration[kKeyboardType];
1028 NSDictionary* autofill = configuration[kAutofillProperties];
1030 self.secureTextEntry = [configuration[kSecureTextEntry] boolValue];
1031 self.enableDeltaModel = [configuration[kEnableDeltaModel] boolValue];
1038 NSString* smartDashesType = configuration[kSmartDashesType];
1040 bool smartDashesIsDisabled = smartDashesType && [smartDashesType isEqualToString:@"0"];
1041 self.smartDashesType = smartDashesIsDisabled ? UITextSmartDashesTypeNo : UITextSmartDashesTypeYes;
1042 NSString* smartQuotesType = configuration[kSmartQuotesType];
1044 bool smartQuotesIsDisabled = smartQuotesType && [smartQuotesType isEqualToString:@"0"];
1045 self.smartQuotesType = smartQuotesIsDisabled ? UITextSmartQuotesTypeNo : UITextSmartQuotesTypeYes;
1047 self.keyboardAppearance = UIKeyboardAppearanceDark;
1049 self.keyboardAppearance = UIKeyboardAppearanceLight;
1051 self.keyboardAppearance = UIKeyboardAppearanceDefault;
1053 NSString* autocorrect = configuration[kAutocorrectionType];
1054 bool autocorrectIsDisabled = autocorrect && ![autocorrect boolValue];
1055 self.autocorrectionType =
1056 autocorrectIsDisabled ? UITextAutocorrectionTypeNo : UITextAutocorrectionTypeDefault;
1057 self.spellCheckingType =
1058 autocorrectIsDisabled ? UITextSpellCheckingTypeNo : UITextSpellCheckingTypeDefault;
1060 if (autofill == nil) {
1061 self.textContentType =
@"";
1064 [
self setTextInputState:autofill[kAutofillEditingValue]];
1065 NSAssert(_autofillId,
@"The autofill configuration must contain an autofill id");
1069 self.isVisibleToAutofill = autofill || _secureTextEntry;
1072 - (UITextContentType)textContentType {
1073 return _textContentType;
1086 - (UIColor*)insertionPointColor {
1087 return [UIColor clearColor];
1090 - (UIColor*)selectionBarColor {
1091 return [UIColor clearColor];
1094 - (UIColor*)selectionHighlightColor {
1095 return [UIColor clearColor];
1098 - (UIInputViewController*)inputViewController {
1110 return _textInputPlugin.textInputDelegate;
1113 - (BOOL)respondsToSelector:(
SEL)selector {
1114 if (@available(iOS 17.0, *)) {
1116 if (selector ==
@selector(insertionPointColor)) {
1120 return [
super respondsToSelector:selector];
1123 - (void)setTextInputClient:(
int)client {
1124 _textInputClient = client;
1128 - (UITextInteraction*)textInteraction
API_AVAILABLE(ios(13.0)) {
1129 if (!_textInteraction) {
1130 _textInteraction = [UITextInteraction textInteractionForMode:UITextInteractionModeEditable];
1131 _textInteraction.textInput =
self;
1133 return _textInteraction;
1136 - (void)setTextInputState:(NSDictionary*)state {
1137 if (@available(iOS 13.0, *)) {
1144 [
self addInteraction:self.textInteraction];
1148 NSString* newText = state[@"text"];
1149 BOOL textChanged = ![
self.text isEqualToString:newText];
1151 [
self.inputDelegate textWillChange:self];
1152 [
self.text setString:newText];
1154 NSInteger composingBase = [state[@"composingBase"] intValue];
1155 NSInteger composingExtent = [state[@"composingExtent"] intValue];
1156 NSRange composingRange = [
self clampSelection:NSMakeRange(MIN(composingBase, composingExtent),
1157 ABS(composingBase - composingExtent))
1160 self.markedTextRange =
1163 NSRange selectedRange = [
self clampSelectionFromBase:[state[@"selectionBase"] intValue]
1164 extent:[state[@"selectionExtent"] intValue]
1167 NSRange oldSelectedRange = [(
FlutterTextRange*)
self.selectedTextRange range];
1168 if (!NSEqualRanges(selectedRange, oldSelectedRange)) {
1169 [
self.inputDelegate selectionWillChange:self];
1177 [
self.inputDelegate selectionDidChange:self];
1181 [
self.inputDelegate textDidChange:self];
1184 if (@available(iOS 13.0, *)) {
1185 if (_textInteraction) {
1186 [
self removeInteraction:_textInteraction];
1192 - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
1193 _scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
1194 [
self resetScribbleInteractionStatusIfEnding];
1195 [
self.viewResponder touchesBegan:touches withEvent:event];
1198 - (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {
1199 [
self.viewResponder touchesMoved:touches withEvent:event];
1202 - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
1203 [
self.viewResponder touchesEnded:touches withEvent:event];
1206 - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
1207 [
self.viewResponder touchesCancelled:touches withEvent:event];
1210 - (void)touchesEstimatedPropertiesUpdated:(NSSet*)touches {
1211 [
self.viewResponder touchesEstimatedPropertiesUpdated:touches];
1221 - (NSRange)clampSelectionFromBase:(
int)selectionBase
1222 extent:(
int)selectionExtent
1223 forText:(NSString*)text {
1224 int loc = MIN(selectionBase, selectionExtent);
1225 int len = ABS(selectionExtent - selectionBase);
1226 return loc < 0 ? NSMakeRange(0, 0)
1227 : [
self clampSelection:NSMakeRange(loc, len) forText:
self.text];
1230 - (NSRange)clampSelection:(NSRange)range forText:(NSString*)text {
1231 NSUInteger start = MIN(MAX(range.location, 0), text.length);
1232 NSUInteger length = MIN(range.length, text.length - start);
1233 return NSMakeRange(start, length);
1236 - (BOOL)isVisibleToAutofill {
1237 return self.frame.size.width > 0 &&
self.frame.size.height > 0;
1245 - (void)setIsVisibleToAutofill:(BOOL)isVisibleToAutofill {
1248 self.frame = isVisibleToAutofill ? CGRectMake(0, 0, 1, 1) : CGRectZero;
1251 #pragma mark UIScribbleInteractionDelegate
1256 if (@available(iOS 14.0, *)) {
1257 if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
1264 - (void)scribbleInteractionWillBeginWriting:(UIScribbleInteraction*)interaction
1265 API_AVAILABLE(ios(14.0)) {
1267 [
self.textInputDelegate flutterTextInputViewScribbleInteractionBegan:self];
1270 - (void)scribbleInteractionDidFinishWriting:(UIScribbleInteraction*)interaction
1271 API_AVAILABLE(ios(14.0)) {
1273 [
self.textInputDelegate flutterTextInputViewScribbleInteractionFinished:self];
1276 - (BOOL)scribbleInteraction:(UIScribbleInteraction*)interaction
1277 shouldBeginAtLocation:(CGPoint)location API_AVAILABLE(ios(14.0)) {
1281 - (BOOL)scribbleInteractionShouldDelayFocus:(UIScribbleInteraction*)interaction
1282 API_AVAILABLE(ios(14.0)) {
1286 #pragma mark - UIResponder Overrides
1288 - (BOOL)canBecomeFirstResponder {
1293 return _textInputClient != 0;
1296 - (BOOL)resignFirstResponder {
1297 BOOL success = [
super resignFirstResponder];
1299 if (!_preventCursorDismissWhenResignFirstResponder) {
1300 [
self.textInputDelegate flutterTextInputView:self
1301 didResignFirstResponderWithTextInputClient:_textInputClient];
1307 - (BOOL)canPerformAction:(
SEL)action withSender:(
id)sender {
1308 if (action ==
@selector(paste:)) {
1310 return [UIPasteboard generalPasteboard].hasStrings;
1311 }
else if (action ==
@selector(copy:) || action ==
@selector(cut:) ||
1312 action ==
@selector(
delete:)) {
1313 return [
self textInRange:_selectedTextRange].length > 0;
1314 }
else if (action ==
@selector(selectAll:)) {
1315 return self.hasText;
1317 return [
super canPerformAction:action withSender:sender];
1320 #pragma mark - UIResponderStandardEditActions Overrides
1322 - (void)cut:(
id)sender {
1323 [UIPasteboard generalPasteboard].string = [
self textInRange:_selectedTextRange];
1324 [
self replaceRange:_selectedTextRange withText:@""];
1327 - (void)copy:(
id)sender {
1328 [UIPasteboard generalPasteboard].string = [
self textInRange:_selectedTextRange];
1331 - (void)paste:(
id)sender {
1332 NSString* pasteboardString = [UIPasteboard generalPasteboard].string;
1333 if (pasteboardString != nil) {
1334 [
self insertText:pasteboardString];
1338 - (void)delete:(
id)sender {
1339 [
self replaceRange:_selectedTextRange withText:@""];
1342 - (void)selectAll:(
id)sender {
1343 [
self setSelectedTextRange:[
self textRangeFromPosition:[
self beginningOfDocument]
1344 toPosition:[
self endOfDocument]]];
1347 #pragma mark - UITextInput Overrides
1349 - (id<UITextInputTokenizer>)tokenizer {
1350 if (_tokenizer == nil) {
1357 return [_selectedTextRange copy];
1361 - (void)setSelectedTextRangeLocal:(UITextRange*)selectedTextRange {
1366 rangeWithNSRange:fml::RangeForCharactersInRange(self.text, flutterTextRange.range)] copy];
1373 - (void)setSelectedTextRange:(UITextRange*)selectedTextRange {
1378 [
self setSelectedTextRangeLocal:selectedTextRange];
1380 if (_enableDeltaModel) {
1381 [
self updateEditingStateWithDelta:flutter::TextEditingDelta([
self.text UTF8String])];
1383 [
self updateEditingState];
1387 _scribbleFocusStatus == FlutterScribbleFocusStatusFocused) {
1391 if (flutterTextRange.
range.length > 0) {
1392 [
self.textInputDelegate flutterTextInputView:self showToolbar:_textInputClient];
1396 [
self resetScribbleInteractionStatusIfEnding];
1399 - (id)insertDictationResultPlaceholder {
1403 - (void)removeDictationResultPlaceholder:(
id)placeholder willInsertResult:(BOOL)willInsertResult {
1406 - (NSString*)textInRange:(UITextRange*)range {
1411 @"Expected a FlutterTextRange for range (got %@).", [range
class]);
1413 if (textRange.location == NSNotFound) {
1422 NSUInteger location = MIN(textRange.location,
self.text.length);
1423 NSUInteger length = MIN(
self.text.length - location, textRange.length);
1424 NSRange safeRange = NSMakeRange(location, length);
1425 return [
self.text substringWithRange:safeRange];
1430 - (void)replaceRangeLocal:(NSRange)range withText:(NSString*)text {
1431 [
self.text replaceCharactersInRange:[
self clampSelection:range forText:self.text]
1437 const NSRange newSelectionRange =
1438 [
self clampSelection:NSMakeRange(range.location + text.length, 0) forText:self.text];
1441 self.markedTextRange = nil;
1444 - (void)replaceRange:(UITextRange*)range withText:(NSString*)text {
1445 NSString* textBeforeChange = [
self.text copy];
1447 [
self replaceRangeLocal:replaceRange withText:text];
1448 if (_enableDeltaModel) {
1449 NSRange nextReplaceRange = [
self clampSelection:replaceRange forText:textBeforeChange];
1450 [
self updateEditingStateWithDelta:flutter::TextEditingDelta(
1451 [textBeforeChange UTF8String],
1453 nextReplaceRange.location,
1454 nextReplaceRange.location + nextReplaceRange.length),
1455 [text UTF8String])];
1457 [
self updateEditingState];
1461 - (BOOL)shouldChangeTextInRange:(UITextRange*)range replacementText:(NSString*)text {
1464 self.temporarilyDeletedComposedCharacter = nil;
1466 if (
self.
returnKeyType == UIReturnKeyDefault && [text isEqualToString:
@"\n"]) {
1467 [
self.textInputDelegate flutterTextInputView:self
1468 performAction:FlutterTextInputActionNewline
1469 withClient:_textInputClient];
1473 if ([text isEqualToString:
@"\n"]) {
1474 FlutterTextInputAction action;
1476 case UIReturnKeyDefault:
1477 action = FlutterTextInputActionUnspecified;
1479 case UIReturnKeyDone:
1480 action = FlutterTextInputActionDone;
1483 action = FlutterTextInputActionGo;
1485 case UIReturnKeySend:
1486 action = FlutterTextInputActionSend;
1488 case UIReturnKeySearch:
1489 case UIReturnKeyGoogle:
1490 case UIReturnKeyYahoo:
1491 action = FlutterTextInputActionSearch;
1493 case UIReturnKeyNext:
1494 action = FlutterTextInputActionNext;
1496 case UIReturnKeyContinue:
1497 action = FlutterTextInputActionContinue;
1499 case UIReturnKeyJoin:
1500 action = FlutterTextInputActionJoin;
1502 case UIReturnKeyRoute:
1503 action = FlutterTextInputActionRoute;
1505 case UIReturnKeyEmergencyCall:
1506 action = FlutterTextInputActionEmergencyCall;
1510 [
self.textInputDelegate flutterTextInputView:self
1511 performAction:action
1512 withClient:_textInputClient];
1521 - (void)setMarkedText:(NSString*)markedText selectedRange:(NSRange)markedSelectedRange {
1522 NSString* textBeforeChange = [
self.text copy];
1525 _scribbleFocusStatus != FlutterScribbleFocusStatusUnfocused) {
1529 if (markedText == nil) {
1534 const NSRange& actualReplacedRange = currentMarkedTextRange && !currentMarkedTextRange.isEmpty
1535 ? currentMarkedTextRange.
range
1539 [
self.text replaceCharactersInRange:actualReplacedRange withString:markedText];
1541 const NSRange newMarkedRange = NSMakeRange(actualReplacedRange.location, markedText.length);
1542 self.markedTextRange =
1545 [
self setSelectedTextRangeLocal:
1547 rangeWithNSRange:[
self clampSelection:NSMakeRange(markedSelectedRange.location +
1548 newMarkedRange.location,
1549 markedSelectedRange.length)
1550 forText:self.text]]];
1551 if (_enableDeltaModel) {
1552 NSRange nextReplaceRange = [
self clampSelection:actualReplacedRange forText:textBeforeChange];
1553 [
self updateEditingStateWithDelta:flutter::TextEditingDelta(
1554 [textBeforeChange UTF8String],
1556 nextReplaceRange.location,
1557 nextReplaceRange.location + nextReplaceRange.length),
1558 [markedText UTF8String])];
1560 [
self updateEditingState];
1564 - (void)unmarkText {
1568 self.markedTextRange = nil;
1569 if (_enableDeltaModel) {
1570 [
self updateEditingStateWithDelta:flutter::TextEditingDelta([
self.text UTF8String])];
1572 [
self updateEditingState];
1576 - (UITextRange*)textRangeFromPosition:(UITextPosition*)fromPosition
1577 toPosition:(UITextPosition*)toPosition {
1580 if (toIndex >= fromIndex) {
1593 - (NSUInteger)decrementOffsetPosition:(NSUInteger)position {
1594 return fml::RangeForCharacterAtIndex(
self.text, MAX(0, position - 1)).location;
1597 - (NSUInteger)incrementOffsetPosition:(NSUInteger)position {
1598 NSRange charRange = fml::RangeForCharacterAtIndex(
self.text, position);
1599 return MIN(position + charRange.length,
self.text.length);
1602 - (UITextPosition*)positionFromPosition:(UITextPosition*)position offset:(NSInteger)offset {
1605 NSInteger newLocation = (NSInteger)offsetPosition + offset;
1606 if (newLocation < 0 || newLocation > (NSInteger)
self.text.length) {
1615 for (NSInteger i = 0; i < offset && offsetPosition <
self.text.length; ++i) {
1616 offsetPosition = [
self incrementOffsetPosition:offsetPosition];
1619 for (NSInteger i = 0; i < ABS(offset) && offsetPosition > 0; ++i) {
1620 offsetPosition = [
self decrementOffsetPosition:offsetPosition];
1626 - (UITextPosition*)positionFromPosition:(UITextPosition*)position
1627 inDirection:(UITextLayoutDirection)direction
1628 offset:(NSInteger)offset {
1630 switch (direction) {
1631 case UITextLayoutDirectionLeft:
1632 case UITextLayoutDirectionUp:
1633 return [
self positionFromPosition:position offset:offset * -1];
1634 case UITextLayoutDirectionRight:
1635 case UITextLayoutDirectionDown:
1636 return [
self positionFromPosition:position offset:1];
1640 - (UITextPosition*)beginningOfDocument {
1644 - (UITextPosition*)endOfDocument {
1646 affinity:UITextStorageDirectionBackward];
1649 - (NSComparisonResult)comparePosition:(UITextPosition*)position toPosition:(UITextPosition*)other {
1652 if (positionIndex < otherIndex) {
1653 return NSOrderedAscending;
1655 if (positionIndex > otherIndex) {
1656 return NSOrderedDescending;
1660 if (positionAffinity == otherAffinity) {
1661 return NSOrderedSame;
1663 if (positionAffinity == UITextStorageDirectionBackward) {
1665 return NSOrderedAscending;
1668 return NSOrderedDescending;
1671 - (NSInteger)offsetFromPosition:(UITextPosition*)from toPosition:(UITextPosition*)toPosition {
1675 - (UITextPosition*)positionWithinRange:(UITextRange*)range
1676 farthestInDirection:(UITextLayoutDirection)direction {
1678 UITextStorageDirection affinity;
1679 switch (direction) {
1680 case UITextLayoutDirectionLeft:
1681 case UITextLayoutDirectionUp:
1683 affinity = UITextStorageDirectionForward;
1685 case UITextLayoutDirectionRight:
1686 case UITextLayoutDirectionDown:
1688 affinity = UITextStorageDirectionBackward;
1694 - (UITextRange*)characterRangeByExtendingPosition:(UITextPosition*)position
1695 inDirection:(UITextLayoutDirection)direction {
1697 NSUInteger startIndex;
1698 NSUInteger endIndex;
1699 switch (direction) {
1700 case UITextLayoutDirectionLeft:
1701 case UITextLayoutDirectionUp:
1702 startIndex = [
self decrementOffsetPosition:positionIndex];
1703 endIndex = positionIndex;
1705 case UITextLayoutDirectionRight:
1706 case UITextLayoutDirectionDown:
1707 startIndex = positionIndex;
1708 endIndex = [
self incrementOffsetPosition:positionIndex];
1714 #pragma mark - UITextInput text direction handling
1716 - (UITextWritingDirection)baseWritingDirectionForPosition:(UITextPosition*)position
1717 inDirection:(UITextStorageDirection)direction {
1719 return UITextWritingDirectionNatural;
1722 - (void)setBaseWritingDirection:(UITextWritingDirection)writingDirection
1723 forRange:(UITextRange*)range {
1727 #pragma mark - UITextInput cursor, selection rect handling
1729 - (void)setMarkedRect:(CGRect)markedRect {
1730 _markedRect = markedRect;
1737 - (void)setEditableTransform:(NSArray*)matrix {
1738 CATransform3D* transform = &_editableTransform;
1740 transform->m11 = [matrix[0] doubleValue];
1741 transform->m12 = [matrix[1] doubleValue];
1742 transform->m13 = [matrix[2] doubleValue];
1743 transform->m14 = [matrix[3] doubleValue];
1745 transform->m21 = [matrix[4] doubleValue];
1746 transform->m22 = [matrix[5] doubleValue];
1747 transform->m23 = [matrix[6] doubleValue];
1748 transform->m24 = [matrix[7] doubleValue];
1750 transform->m31 = [matrix[8] doubleValue];
1751 transform->m32 = [matrix[9] doubleValue];
1752 transform->m33 = [matrix[10] doubleValue];
1753 transform->m34 = [matrix[11] doubleValue];
1755 transform->m41 = [matrix[12] doubleValue];
1756 transform->m42 = [matrix[13] doubleValue];
1757 transform->m43 = [matrix[14] doubleValue];
1758 transform->m44 = [matrix[15] doubleValue];
1767 CGPoint points[] = {
1768 incomingRect.origin,
1769 CGPointMake(incomingRect.origin.x, incomingRect.origin.y + incomingRect.size.height),
1770 CGPointMake(incomingRect.origin.x + incomingRect.size.width, incomingRect.origin.y),
1771 CGPointMake(incomingRect.origin.x + incomingRect.size.width,
1772 incomingRect.origin.y + incomingRect.size.height)};
1774 CGPoint origin = CGPointMake(CGFLOAT_MAX, CGFLOAT_MAX);
1775 CGPoint farthest = CGPointMake(-CGFLOAT_MAX, -CGFLOAT_MAX);
1777 for (
int i = 0; i < 4; i++) {
1778 const CGPoint point = points[i];
1780 CGFloat x = _editableTransform.m11 * point.x + _editableTransform.m21 * point.y +
1781 _editableTransform.m41;
1782 CGFloat y = _editableTransform.m12 * point.x + _editableTransform.m22 * point.y +
1783 _editableTransform.m42;
1785 const CGFloat w = _editableTransform.m14 * point.x + _editableTransform.m24 * point.y +
1786 _editableTransform.m44;
1790 }
else if (w != 1.0) {
1795 origin.x = MIN(origin.x, x);
1796 origin.y = MIN(origin.y, y);
1797 farthest.x = MAX(farthest.x, x);
1798 farthest.y = MAX(farthest.y, y);
1800 return CGRectMake(origin.x, origin.y, farthest.x - origin.x, farthest.y - origin.y);
1809 - (CGRect)firstRectForRange:(UITextRange*)range {
1811 @"Expected a FlutterTextPosition for range.start (got %@).", [range.start
class]);
1813 @"Expected a FlutterTextPosition for range.end (got %@).", [range.end
class]);
1816 if (_markedTextRange != nil) {
1827 CGRect rect = _markedRect;
1828 if (CGRectIsEmpty(rect)) {
1829 rect = CGRectInset(rect, -0.1, 0);
1834 UIView* hostView = _textInputPlugin.hostView;
1835 NSAssert(hostView == nil || [
self isDescendantOfView:hostView],
@"%@ is not a descendant of %@",
1837 return hostView ? [hostView convertRect:_cachedFirstRect toView:self] :
_cachedFirstRect;
1841 _scribbleFocusStatus == FlutterScribbleFocusStatusUnfocused) {
1842 if (@available(iOS 17.0, *)) {
1852 [
self.textInputDelegate flutterTextInputView:self
1853 showAutocorrectionPromptRectForStart:start
1855 withClient:_textInputClient];
1863 if (@available(iOS 17, *)) {
1869 NSUInteger first = start;
1874 CGRect startSelectionRect = CGRectNull;
1875 CGRect endSelectionRect = CGRectNull;
1878 CGFloat minY = CGFLOAT_MAX;
1879 CGFloat maxY = CGFLOAT_MIN;
1882 rangeWithNSRange:fml::RangeForCharactersInRange(self.text, NSMakeRange(0, self.text.length))];
1883 for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
1884 BOOL startsOnOrBeforeStartOfRange = _selectionRects[i].position <= first;
1885 BOOL isLastSelectionRect = i + 1 == [_selectionRects count];
1886 BOOL endOfTextIsAfterStartOfRange = isLastSelectionRect && textRange.
range.length > first;
1887 BOOL nextSelectionRectIsAfterStartOfRange =
1888 !isLastSelectionRect && _selectionRects[i + 1].position > first;
1889 if (startsOnOrBeforeStartOfRange &&
1890 (endOfTextIsAfterStartOfRange || nextSelectionRectIsAfterStartOfRange)) {
1892 if (@available(iOS 17, *)) {
1893 startSelectionRect = _selectionRects[i].rect;
1895 return _selectionRects[i].rect;
1898 if (!CGRectIsNull(startSelectionRect)) {
1899 minY = fmin(minY, CGRectGetMinY(_selectionRects[i].rect));
1900 maxY = fmax(maxY, CGRectGetMaxY(_selectionRects[i].rect));
1901 BOOL endsOnOrAfterEndOfRange = _selectionRects[i].position >= end - 1;
1902 BOOL nextSelectionRectIsOnNextLine =
1903 !isLastSelectionRect &&
1908 CGRectGetMidY(_selectionRects[i + 1].rect) > CGRectGetMaxY(_selectionRects[i].rect);
1909 if (endsOnOrAfterEndOfRange || isLastSelectionRect || nextSelectionRectIsOnNextLine) {
1910 endSelectionRect = _selectionRects[i].rect;
1915 if (CGRectIsNull(startSelectionRect) || CGRectIsNull(endSelectionRect)) {
1919 CGFloat minX = fmin(CGRectGetMinX(startSelectionRect), CGRectGetMinX(endSelectionRect));
1920 CGFloat maxX = fmax(CGRectGetMaxX(startSelectionRect), CGRectGetMaxX(endSelectionRect));
1921 return CGRectMake(minX, minY, maxX - minX, maxY - minY);
1929 NSArray<UITextSelectionRect*>* rects = [
self
1931 rangeWithNSRange:fml::RangeForCharactersInRange(
1935 (index >= (NSInteger)self.text.length)
1938 if (rects.count == 0) {
1944 CGRect characterAfterCaret = rects[0].rect;
1949 return CGRectMake(characterAfterCaret.origin.x + characterAfterCaret.size.width,
1950 characterAfterCaret.origin.y, 0, characterAfterCaret.size.height);
1952 return CGRectMake(characterAfterCaret.origin.x, characterAfterCaret.origin.y, 0,
1953 characterAfterCaret.size.height);
1955 }
else if (rects.count == 2 && affinity == UITextStorageDirectionForward) {
1958 CGRect characterAfterCaret = rects[1].rect;
1963 return CGRectMake(characterAfterCaret.origin.x + characterAfterCaret.size.width,
1964 characterAfterCaret.origin.y, 0, characterAfterCaret.size.height);
1966 return CGRectMake(characterAfterCaret.origin.x, characterAfterCaret.origin.y, 0,
1967 characterAfterCaret.size.height);
1976 CGRect characterBeforeCaret = rects[0].rect;
1979 return CGRectMake(characterBeforeCaret.origin.x, characterBeforeCaret.origin.y, 0,
1980 characterBeforeCaret.size.height);
1982 return CGRectMake(characterBeforeCaret.origin.x + characterBeforeCaret.size.width,
1983 characterBeforeCaret.origin.y, 0, characterBeforeCaret.size.height);
1987 - (UITextPosition*)closestPositionToPoint:(CGPoint)point {
1988 if ([_selectionRects count] == 0) {
1990 @"Expected a FlutterTextPosition for position (got %@).",
1993 UITextStorageDirection currentAffinity =
1999 rangeWithNSRange:fml::RangeForCharactersInRange(self.text, NSMakeRange(0, self.text.length))];
2000 return [
self closestPositionToPoint:point withinRange:range];
2003 - (NSArray*)selectionRectsForRange:(UITextRange*)range {
2011 @"Expected a FlutterTextPosition for range.start (got %@).", [range.start
class]);
2013 @"Expected a FlutterTextPosition for range.end (got %@).", [range.end
class]);
2016 NSMutableArray* rects = [[NSMutableArray alloc] init];
2017 for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
2018 if (_selectionRects[i].position >= start &&
2019 (_selectionRects[i].position < end ||
2020 (start == end && _selectionRects[i].position <= end))) {
2021 float width = _selectionRects[i].rect.size.width;
2025 CGRect rect = CGRectMake(_selectionRects[i].rect.origin.x, _selectionRects[i].rect.origin.y,
2026 width, _selectionRects[i].rect.size.height);
2029 position:_selectionRects[i].position
2033 self.text, NSMakeRange(0, self.text.length))
2036 [rects addObject:selectionRect];
2042 - (UITextPosition*)closestPositionToPoint:(CGPoint)point withinRange:(UITextRange*)range {
2044 @"Expected a FlutterTextPosition for range.start (got %@).", [range.start
class]);
2046 @"Expected a FlutterTextPosition for range.end (got %@).", [range.end
class]);
2056 NSUInteger _closestRectIndex = 0;
2057 for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
2058 NSUInteger position = _selectionRects[i].position;
2059 if (position >= start && position <= end) {
2062 point, _selectionRects[i].rect, _selectionRects[i].isRTL,
2063 NO, _selectionRects[_closestRectIndex].rect,
2064 _selectionRects[_closestRectIndex].isRTL, verticalPrecision)) {
2066 _closestRectIndex = i;
2073 affinity:UITextStorageDirectionForward];
2079 for (NSUInteger i = MAX(0, _closestRectIndex - 1);
2080 i < MIN(_closestRectIndex + 2, [_selectionRects count]); i++) {
2081 NSUInteger position = _selectionRects[i].position + 1;
2082 if (position >= start && position <= end) {
2084 point, _selectionRects[i].rect, _selectionRects[i].isRTL,
2085 YES, _selectionRects[_closestRectIndex].rect,
2086 _selectionRects[_closestRectIndex].isRTL, verticalPrecision)) {
2089 affinity:UITextStorageDirectionBackward];
2094 return closestPosition;
2097 - (UITextRange*)characterRangeAtPoint:(CGPoint)point {
2100 return [
FlutterTextRange rangeWithNSRange:fml::RangeForCharacterAtIndex(self.text, currentIndex)];
2131 - (void)beginFloatingCursorAtPoint:(CGPoint)point {
2148 [
self.textInputDelegate flutterTextInputView:self
2149 updateFloatingCursor:FlutterFloatingCursorDragStateStart
2150 withClient:_textInputClient
2151 withPosition:@{@"X" : @0, @"Y" : @0}];
2154 - (void)updateFloatingCursorAtPoint:(CGPoint)point {
2155 [
self.textInputDelegate flutterTextInputView:self
2156 updateFloatingCursor:FlutterFloatingCursorDragStateUpdate
2157 withClient:_textInputClient
2159 @"X" : @(point.x - _floatingCursorOffset.x),
2160 @"Y" : @(point.y - _floatingCursorOffset.y)
2164 - (void)endFloatingCursor {
2166 [
self.textInputDelegate flutterTextInputView:self
2167 updateFloatingCursor:FlutterFloatingCursorDragStateEnd
2168 withClient:_textInputClient
2169 withPosition:@{@"X" : @0, @"Y" : @0}];
2172 #pragma mark - UIKeyInput Overrides
2174 - (void)updateEditingState {
2179 NSInteger composingBase = -1;
2180 NSInteger composingExtent = -1;
2185 NSDictionary* state = @{
2186 @"selectionBase" : @(selectionBase),
2187 @"selectionExtent" : @(selectionExtent),
2189 @"selectionIsDirectional" : @(
false),
2190 @"composingBase" : @(composingBase),
2191 @"composingExtent" : @(composingExtent),
2192 @"text" : [NSString stringWithString:self.text],
2195 if (_textInputClient == 0 && _autofillId != nil) {
2196 [
self.textInputDelegate flutterTextInputView:self
2197 updateEditingClient:_textInputClient
2199 withTag:_autofillId];
2201 [
self.textInputDelegate flutterTextInputView:self
2202 updateEditingClient:_textInputClient
2207 - (void)updateEditingStateWithDelta:(
flutter::TextEditingDelta)delta {
2212 NSInteger composingBase = -1;
2213 NSInteger composingExtent = -1;
2219 NSDictionary* deltaToFramework = @{
2220 @"oldText" : @(delta.old_text().c_str()),
2221 @"deltaText" : @(delta.delta_text().c_str()),
2222 @"deltaStart" : @(delta.delta_start()),
2223 @"deltaEnd" : @(delta.delta_end()),
2224 @"selectionBase" : @(selectionBase),
2225 @"selectionExtent" : @(selectionExtent),
2227 @"selectionIsDirectional" : @(
false),
2228 @"composingBase" : @(composingBase),
2229 @"composingExtent" : @(composingExtent),
2232 [_pendingDeltas addObject:deltaToFramework];
2234 if (_pendingDeltas.count == 1) {
2236 dispatch_async(dispatch_get_main_queue(), ^{
2238 if (strongSelf && strongSelf.pendingDeltas.count > 0) {
2239 NSDictionary* deltas = @{
2240 @"deltas" : strongSelf.pendingDeltas,
2243 [strongSelf.textInputDelegate flutterTextInputView:strongSelf
2244 updateEditingClient:strongSelf->_textInputClient
2246 [strongSelf.pendingDeltas removeAllObjects];
2253 return self.text.length > 0;
2256 - (void)insertText:(NSString*)text {
2257 if (
self.temporarilyDeletedComposedCharacter.length > 0 && text.length == 1 && !text.UTF8String &&
2258 [text characterAtIndex:0] == [
self.temporarilyDeletedComposedCharacter characterAtIndex:0]) {
2262 text =
self.temporarilyDeletedComposedCharacter;
2263 self.temporarilyDeletedComposedCharacter = nil;
2266 NSMutableArray<FlutterTextSelectionRect*>* copiedRects =
2267 [[NSMutableArray alloc] initWithCapacity:[_selectionRects count]];
2269 @"Expected a FlutterTextPosition for position (got %@).",
2272 for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
2273 NSUInteger rectPosition = _selectionRects[i].position;
2274 if (rectPosition == insertPosition) {
2275 for (NSUInteger j = 0; j <= text.length; j++) {
2282 if (rectPosition > insertPosition) {
2283 rectPosition = rectPosition + text.length;
2292 _scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
2293 [
self resetScribbleInteractionStatusIfEnding];
2294 self.selectionRects = copiedRects;
2296 [
self replaceRange:_selectedTextRange withText:text];
2299 - (UITextPlaceholder*)insertTextPlaceholderWithSize:(CGSize)size API_AVAILABLE(ios(13.0)) {
2300 [
self.textInputDelegate flutterTextInputView:self
2301 insertTextPlaceholderWithSize:size
2302 withClient:_textInputClient];
2307 - (void)removeTextPlaceholder:(UITextPlaceholder*)textPlaceholder API_AVAILABLE(ios(13.0)) {
2309 [
self.textInputDelegate flutterTextInputView:self removeTextPlaceholder:_textInputClient];
2312 - (void)deleteBackward {
2314 _scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
2315 [
self resetScribbleInteractionStatusIfEnding];
2332 if (oldRange.location > 0) {
2333 NSRange newRange = NSMakeRange(oldRange.location - 1, 1);
2337 NSRange charRange = fml::RangeForCharacterAtIndex(
self.text, oldRange.location - 1);
2338 if (
IsEmoji(
self.text, charRange)) {
2339 newRange = NSMakeRange(charRange.location, oldRange.location - charRange.location);
2351 NSString* deletedText = [
self.text substringWithRange:_selectedTextRange.range];
2352 NSRange deleteFirstCharacterRange = fml::RangeForCharacterAtIndex(deletedText, 0);
2353 self.temporarilyDeletedComposedCharacter =
2354 [deletedText substringWithRange:deleteFirstCharacterRange];
2356 [
self replaceRange:_selectedTextRange withText:@""];
2360 - (void)postAccessibilityNotification:(UIAccessibilityNotifications)notification target:(
id)target {
2361 UIAccessibilityPostNotification(notification, target);
2364 - (void)accessibilityElementDidBecomeFocused {
2365 if ([
self accessibilityElementIsFocused]) {
2369 FML_DCHECK(_backingTextInputAccessibilityObject);
2370 [
self postAccessibilityNotification:UIAccessibilityScreenChangedNotification
2371 target:_backingTextInputAccessibilityObject];
2375 - (BOOL)accessibilityElementsHidden {
2376 return !_accessibilityEnabled;
2385 #pragma mark - Key Events Handling
2386 - (void)pressesBegan:(NSSet<UIPress*>*)presses
2387 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2388 [_textInputPlugin.viewController pressesBegan:presses withEvent:event];
2391 - (void)pressesChanged:(NSSet<UIPress*>*)presses
2392 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2393 [_textInputPlugin.viewController pressesChanged:presses withEvent:event];
2396 - (void)pressesEnded:(NSSet<UIPress*>*)presses
2397 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2398 [_textInputPlugin.viewController pressesEnded:presses withEvent:event];
2401 - (void)pressesCancelled:(NSSet<UIPress*>*)presses
2402 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2403 [_textInputPlugin.viewController pressesCancelled:presses withEvent:event];
2432 - (BOOL)accessibilityElementsHidden {
2439 - (void)enableActiveViewAccessibility;
2456 - (void)enableActiveViewAccessibility {
2457 [
self.target enableActiveViewAccessibility];
2464 @property(nonatomic, readonly)
2465 NSMutableDictionary<NSString*, FlutterTextInputView*>* autofillContext;
2470 @property(nonatomic, strong) UIView* keyboardViewContainer;
2471 @property(nonatomic, strong) UIView* keyboardView;
2472 @property(nonatomic, strong) UIView* cachedFirstResponder;
2473 @property(nonatomic, assign) CGRect keyboardRect;
2474 @property(nonatomic, assign) CGFloat previousPointerYPosition;
2475 @property(nonatomic, assign) CGFloat pointerYVelocity;
2479 NSTimer* _enableFlutterTextInputViewAccessibilityTimer;
2483 self = [
super init];
2486 _textInputDelegate = textInputDelegate;
2487 _autofillContext = [[NSMutableDictionary alloc] init];
2489 _scribbleElements = [[NSMutableDictionary alloc] init];
2490 _keyboardViewContainer = [[UIView alloc] init];
2492 [[NSNotificationCenter defaultCenter] addObserver:self
2493 selector:@selector(handleKeyboardWillShow:)
2494 name:UIKeyboardWillShowNotification
2501 - (void)handleKeyboardWillShow:(NSNotification*)notification {
2502 NSDictionary* keyboardInfo = [notification userInfo];
2503 NSValue* keyboardFrameEnd = [keyboardInfo valueForKey:UIKeyboardFrameEndUserInfoKey];
2504 _keyboardRect = [keyboardFrameEnd CGRectValue];
2508 [
self hideTextInput];
2511 - (void)removeEnableFlutterTextInputViewAccessibilityTimer {
2512 if (_enableFlutterTextInputViewAccessibilityTimer) {
2513 [_enableFlutterTextInputViewAccessibilityTimer invalidate];
2514 _enableFlutterTextInputViewAccessibilityTimer = nil;
2518 - (UIView<UITextInput>*)textInputView {
2523 NSString* method = call.
method;
2526 [
self showTextInput];
2528 }
else if ([method isEqualToString:
kHideMethod]) {
2529 [
self hideTextInput];
2532 [
self setTextInputClient:[args[0] intValue] withConfiguration:args[1]];
2536 [
self setPlatformViewTextInputClient];
2539 [
self setTextInputEditingState:args];
2542 [
self clearTextInputClient];
2545 [
self setEditableSizeAndTransform:args];
2548 [
self updateMarkedRect:args];
2551 [
self triggerAutofillSave:[args boolValue]];
2557 [
self setSelectionRects:args];
2560 [
self setSelectionRects:args];
2563 [
self startLiveTextInput];
2566 [
self updateConfig:args];
2569 CGFloat pointerY = (CGFloat)[args[
@"pointerY"] doubleValue];
2570 [
self handlePointerMove:pointerY];
2573 CGFloat pointerY = (CGFloat)[args[
@"pointerY"] doubleValue];
2574 [
self handlePointerUp:pointerY];
2581 - (void)handlePointerUp:(CGFloat)pointerY {
2582 if (_keyboardView.superview != nil) {
2585 UIScreen* screen = _viewController.flutterScreenIfViewLoaded;
2586 CGFloat screenHeight = screen.bounds.size.height;
2587 CGFloat keyboardHeight = _keyboardRect.size.height;
2589 BOOL shouldDismissKeyboardBasedOnVelocity = _pointerYVelocity < 0;
2590 [UIView animateWithDuration:kKeyboardAnimationTimeToCompleteion
2592 double keyboardDestination =
2593 shouldDismissKeyboardBasedOnVelocity ? screenHeight : screenHeight - keyboardHeight;
2594 _keyboardViewContainer.frame = CGRectMake(
2595 0, keyboardDestination, _viewController.flutterScreenIfViewLoaded.bounds.size.width,
2596 _keyboardViewContainer.frame.size.height);
2598 completion:^(BOOL finished) {
2599 if (shouldDismissKeyboardBasedOnVelocity) {
2600 [
self.textInputDelegate flutterTextInputView:self.activeView
2601 didResignFirstResponderWithTextInputClient:self.activeView.textInputClient];
2602 [
self dismissKeyboardScreenshot];
2604 [
self showKeyboardAndRemoveScreenshot];
2610 - (void)dismissKeyboardScreenshot {
2611 for (UIView* subView in _keyboardViewContainer.subviews) {
2612 [subView removeFromSuperview];
2616 - (void)showKeyboardAndRemoveScreenshot {
2617 [UIView setAnimationsEnabled:NO];
2618 [_cachedFirstResponder becomeFirstResponder];
2622 dispatch_get_main_queue(), ^{
2623 [UIView setAnimationsEnabled:YES];
2624 [
self dismissKeyboardScreenshot];
2628 - (void)handlePointerMove:(CGFloat)pointerY {
2630 UIScreen* screen = _viewController.flutterScreenIfViewLoaded;
2631 CGFloat screenHeight = screen.bounds.size.height;
2632 CGFloat keyboardHeight = _keyboardRect.size.height;
2633 if (screenHeight - keyboardHeight <= pointerY) {
2635 if (_keyboardView.superview == nil) {
2637 [
self takeKeyboardScreenshotAndDisplay];
2638 [
self hideKeyboardWithoutAnimationAndAvoidCursorDismissUpdate];
2640 [
self setKeyboardContainerHeight:pointerY];
2641 _pointerYVelocity = _previousPointerYPosition - pointerY;
2644 if (_keyboardView.superview != nil) {
2646 _keyboardViewContainer.frame = _keyboardRect;
2647 _pointerYVelocity = _previousPointerYPosition - pointerY;
2650 _previousPointerYPosition = pointerY;
2653 - (void)setKeyboardContainerHeight:(CGFloat)pointerY {
2654 CGRect frameRect = _keyboardRect;
2655 frameRect.origin.y = pointerY;
2656 _keyboardViewContainer.frame = frameRect;
2659 - (void)hideKeyboardWithoutAnimationAndAvoidCursorDismissUpdate {
2660 [UIView setAnimationsEnabled:NO];
2662 _cachedFirstResponder =
2664 ? flutterApplication.keyWindow.flutterFirstResponder
2665 :
self.viewController.flutterWindowSceneIfViewLoaded.keyWindow.flutterFirstResponder;
2667 _activeView.preventCursorDismissWhenResignFirstResponder = YES;
2668 [_cachedFirstResponder resignFirstResponder];
2669 _activeView.preventCursorDismissWhenResignFirstResponder = NO;
2670 [UIView setAnimationsEnabled:YES];
2673 - (void)takeKeyboardScreenshotAndDisplay {
2675 UIScreen* screen = _viewController.flutterScreenIfViewLoaded;
2676 UIView* keyboardSnap = [screen snapshotViewAfterScreenUpdates:YES];
2677 keyboardSnap = [keyboardSnap resizableSnapshotViewFromRect:_keyboardRect
2678 afterScreenUpdates:YES
2679 withCapInsets:UIEdgeInsetsZero];
2680 _keyboardView = keyboardSnap;
2681 [_keyboardViewContainer addSubview:_keyboardView];
2682 if (_keyboardViewContainer.superview == nil) {
2684 UIView* rootView = flutterApplication
2685 ? flutterApplication.delegate.window.rootViewController.view
2686 :
self.viewController.viewIfLoaded.window.rootViewController.view;
2687 [rootView addSubview:_keyboardViewContainer];
2689 _keyboardViewContainer.layer.zPosition = NSIntegerMax;
2690 _keyboardViewContainer.frame = _keyboardRect;
2693 - (BOOL)showEditMenu:(NSDictionary*)args API_AVAILABLE(ios(16.0)) {
2694 if (!
self.activeView.isFirstResponder) {
2697 NSDictionary<NSString*, NSNumber*>* encodedTargetRect = args[@"targetRect"];
2698 CGRect globalTargetRect = CGRectMake(
2699 [encodedTargetRect[
@"x"] doubleValue], [encodedTargetRect[
@"y"] doubleValue],
2700 [encodedTargetRect[
@"width"] doubleValue], [encodedTargetRect[
@"height"] doubleValue]);
2701 CGRect localTargetRect = [
self.hostView convertRect:globalTargetRect toView:self.activeView];
2702 [
self.activeView showEditMenuWithTargetRect:localTargetRect items:args[@"items"]];
2706 - (void)hideEditMenu {
2707 [
self.activeView hideEditMenu];
2710 - (void)setEditableSizeAndTransform:(NSDictionary*)dictionary {
2711 NSArray* transform = dictionary[@"transform"];
2712 [_activeView setEditableTransform:transform];
2713 const int leftIndex = 12;
2714 const int topIndex = 13;
2718 CGRectMake([transform[leftIndex] intValue], [transform[topIndex] intValue],
2719 [dictionary[
@"width"] intValue], [dictionary[
@"height"] intValue]);
2721 CGRectMake(0, 0, [dictionary[
@"width"] intValue], [dictionary[
@"height"] intValue]);
2722 _activeView.tintColor = [UIColor clearColor];
2727 if (@available(iOS 17, *)) {
2735 CGRectMake([transform[leftIndex] intValue], [transform[topIndex] intValue], 0, 0);
2740 - (void)updateMarkedRect:(NSDictionary*)dictionary {
2741 NSAssert(dictionary[
@"x"] != nil && dictionary[
@"y"] != nil && dictionary[
@"width"] != nil &&
2742 dictionary[
@"height"] != nil,
2743 @"Expected a dictionary representing a CGRect, got %@", dictionary);
2744 CGRect rect = CGRectMake([dictionary[
@"x"] doubleValue], [dictionary[
@"y"] doubleValue],
2745 [dictionary[
@"width"] doubleValue], [dictionary[
@"height"] doubleValue]);
2746 _activeView.markedRect = rect.size.width < 0 && rect.size.height < 0 ?
kInvalidFirstRect : rect;
2749 - (void)setSelectionRects:(NSArray*)encodedRects {
2750 NSMutableArray<FlutterTextSelectionRect*>* rectsAsRect =
2751 [[NSMutableArray alloc] initWithCapacity:[encodedRects count]];
2752 for (NSUInteger i = 0; i < [encodedRects count]; i++) {
2753 NSArray<NSNumber*>* encodedRect = encodedRects[i];
2755 selectionRectWithRect:CGRectMake([encodedRect[0] floatValue],
2756 [encodedRect[1] floatValue],
2757 [encodedRect[2] floatValue],
2758 [encodedRect[3] floatValue])
2759 position:[encodedRect[4] unsignedIntegerValue]
2760 writingDirection:[encodedRect[5] unsignedIntegerValue] == 1
2761 ? NSWritingDirectionLeftToRight
2762 : NSWritingDirectionRightToLeft]];
2768 _activeView.selectionRects = rectsAsRect;
2771 - (void)startLiveTextInput {
2772 if (@available(iOS 15.0, *)) {
2773 if (_activeView == nil || !_activeView.isFirstResponder) {
2776 [_activeView captureTextFromCamera:nil];
2780 - (void)showTextInput {
2781 _activeView.viewResponder = _viewResponder;
2782 [
self addToInputParentViewIfNeeded:_activeView];
2791 if (!_enableFlutterTextInputViewAccessibilityTimer) {
2792 _enableFlutterTextInputViewAccessibilityTimer =
2793 [NSTimer scheduledTimerWithTimeInterval:kUITextInputAccessibilityEnablingDelaySeconds
2795 selector:@selector(enableActiveViewAccessibility)
2799 [_activeView becomeFirstResponder];
2802 - (void)enableActiveViewAccessibility {
2803 if (_activeView.isFirstResponder) {
2804 _activeView.accessibilityEnabled = YES;
2806 [
self removeEnableFlutterTextInputViewAccessibilityTimer];
2809 - (void)hideTextInput {
2810 [
self removeEnableFlutterTextInputViewAccessibilityTimer];
2811 _activeView.accessibilityEnabled = NO;
2812 [_activeView resignFirstResponder];
2817 [
self cleanUpViewHierarchy:YES clearText:YES delayRemoval:NO];
2820 - (void)triggerAutofillSave:(BOOL)saveEntries {
2821 [_activeView resignFirstResponder];
2826 [
self cleanUpViewHierarchy:YES clearText:YES delayRemoval:NO];
2827 [_autofillContext removeAllObjects];
2828 [
self changeInputViewsAutofillVisibility:YES];
2830 [_autofillContext removeAllObjects];
2833 [
self cleanUpViewHierarchy:YES clearText:!saveEntries delayRemoval:NO];
2834 [
self addToInputParentViewIfNeeded:_activeView];
2837 - (void)setPlatformViewTextInputClient {
2841 [
self removeEnableFlutterTextInputViewAccessibilityTimer];
2842 _activeView.accessibilityEnabled = NO;
2843 [_activeView removeFromSuperview];
2844 [_inputHider removeFromSuperview];
2847 - (void)setTextInputClient:(
int)client withConfiguration:(NSDictionary*)configuration {
2848 [
self resetAllClientIds];
2851 [
self changeInputViewsAutofillVisibility:NO];
2855 case kFlutterAutofillTypeNone:
2856 self.activeView = [
self createInputViewWith:configuration];
2858 case kFlutterAutofillTypeRegular:
2861 self.activeView = [
self updateAndShowAutofillViews:nil
2862 focusedField:configuration
2863 isPasswordRelated:NO];
2865 case kFlutterAutofillTypePassword:
2866 self.activeView = [
self updateAndShowAutofillViews:configuration[kAssociatedAutofillFields]
2867 focusedField:configuration
2868 isPasswordRelated:YES];
2871 [_activeView setTextInputClient:client];
2872 [_activeView reloadInputViews];
2884 [
self cleanUpViewHierarchy:NO clearText:YES delayRemoval:YES];
2895 [_autofillContext removeObjectForKey:autofillId];
2898 [newView configureWithDictionary:configuration];
2899 [
self addToInputParentViewIfNeeded:newView];
2903 if (autofillId &&
AutofillTypeOf(field) == kFlutterAutofillTypeNone) {
2904 [_autofillContext removeObjectForKey:autofillId];
2911 focusedField:(NSDictionary*)focusedField
2912 isPasswordRelated:(BOOL)isPassword {
2915 NSAssert(focusedId,
@"autofillId must not be null for the focused field: %@", focusedField);
2920 focused = [
self getOrCreateAutofillableView:focusedField isPasswordAutofill:isPassword];
2921 [_autofillContext removeObjectForKey:focusedId];
2924 for (NSDictionary* field in fields) {
2926 NSAssert(autofillId,
@"autofillId must not be null for field: %@", field);
2928 BOOL hasHints =
AutofillTypeOf(field) != kFlutterAutofillTypeNone;
2929 BOOL isFocused = [focusedId isEqualToString:autofillId];
2932 focused = [
self getOrCreateAutofillableView:field isPasswordAutofill:isPassword];
2937 _autofillContext[autofillId] = isFocused ? focused
2938 : [
self getOrCreateAutofillableView:field
2939 isPasswordAutofill:isPassword];
2942 [_autofillContext removeObjectForKey:autofillId];
2946 NSAssert(focused,
@"The current focused input view must not be nil.");
2956 isPasswordAutofill:(BOOL)needsPasswordAutofill {
2962 inputView = [inputView initWithOwner:self];
2963 [
self addToInputParentViewIfNeeded:inputView];
2966 [inputView configureWithDictionary:field];
2971 - (UIView*)hostView {
2972 UIView* host = _viewController.view;
2973 NSAssert(host !=
nullptr,
2974 @"The application must have a host view since the keyboard client "
2975 @"must be part of the responder chain to function. The host view controller is %@",
2981 - (NSArray<UIView*>*)textInputViews {
2982 return _inputHider.subviews;
2995 - (void)cleanUpViewHierarchy:(BOOL)includeActiveView
2996 clearText:(BOOL)clearText
2997 delayRemoval:(BOOL)delayRemoval {
2998 for (UIView* view in
self.textInputViews) {
3000 (includeActiveView || view != _activeView)) {
3002 if (_autofillContext[inputView.autofillId] != view) {
3004 [inputView replaceRangeLocal:NSMakeRange(0, inputView.text.length) withText:@""];
3007 [inputView performSelector:@selector(removeFromSuperview) withObject:nil afterDelay:0.1];
3009 [inputView removeFromSuperview];
3018 - (void)changeInputViewsAutofillVisibility:(BOOL)newVisibility {
3019 for (UIView* view in
self.textInputViews) {
3022 inputView.isVisibleToAutofill = newVisibility;
3034 - (void)resetAllClientIds {
3035 for (UIView* view in
self.textInputViews) {
3038 [inputView setTextInputClient:0];
3044 if (![inputView isDescendantOfView:_inputHider]) {
3045 [_inputHider addSubview:inputView];
3048 if (_viewController.view == nil) {
3054 UIView* parentView =
self.hostView;
3055 if (_inputHider.superview != parentView) {
3056 [parentView addSubview:_inputHider];
3060 - (void)setTextInputEditingState:(NSDictionary*)state {
3061 [_activeView setTextInputState:state];
3064 - (void)clearTextInputClient {
3065 [_activeView setTextInputClient:0];
3066 _activeView.frame = CGRectZero;
3069 - (void)updateConfig:(NSDictionary*)dictionary {
3070 BOOL isSecureTextEntry = [dictionary[kSecureTextEntry] boolValue];
3071 for (UIView* view in
self.textInputViews) {
3078 if (inputView.isSecureTextEntry != isSecureTextEntry) {
3079 inputView.secureTextEntry = isSecureTextEntry;
3080 [inputView reloadInputViews];
3086 #pragma mark UIIndirectScribbleInteractionDelegate
3088 - (BOOL)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
3089 isElementFocused:(UIScribbleElementIdentifier)elementIdentifier
3090 API_AVAILABLE(ios(14.0)) {
3091 return _activeView.scribbleFocusStatus == FlutterScribbleFocusStatusFocused;
3094 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
3095 focusElementIfNeeded:(UIScribbleElementIdentifier)elementIdentifier
3096 referencePoint:(CGPoint)focusReferencePoint
3097 completion:(
void (^)(UIResponder<UITextInput>* focusedInput))completion
3098 API_AVAILABLE(ios(14.0)) {
3099 _activeView.scribbleFocusStatus = FlutterScribbleFocusStatusFocusing;
3100 [_indirectScribbleDelegate flutterTextInputPlugin:self
3101 focusElement:elementIdentifier
3102 atPoint:focusReferencePoint
3103 result:^(id _Nullable result) {
3104 _activeView.scribbleFocusStatus =
3105 FlutterScribbleFocusStatusFocused;
3106 completion(_activeView);
3110 - (BOOL)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
3111 shouldDelayFocusForElement:(UIScribbleElementIdentifier)elementIdentifier
3112 API_AVAILABLE(ios(14.0)) {
3116 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
3117 willBeginWritingInElement:(UIScribbleElementIdentifier)elementIdentifier
3118 API_AVAILABLE(ios(14.0)) {
3121 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
3122 didFinishWritingInElement:(UIScribbleElementIdentifier)elementIdentifier
3123 API_AVAILABLE(ios(14.0)) {
3126 - (CGRect)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
3127 frameForElement:(UIScribbleElementIdentifier)elementIdentifier
3128 API_AVAILABLE(ios(14.0)) {
3129 NSValue* elementValue = [_scribbleElements objectForKey:elementIdentifier];
3130 if (elementValue == nil) {
3133 return [elementValue CGRectValue];
3136 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
3137 requestElementsInRect:(CGRect)rect
3139 (
void (^)(NSArray<UIScribbleElementIdentifier>* elements))completion
3140 API_AVAILABLE(ios(14.0)) {
3141 [_indirectScribbleDelegate
3142 flutterTextInputPlugin:self
3143 requestElementsInRect:rect
3144 result:^(id _Nullable result) {
3145 NSMutableArray<UIScribbleElementIdentifier>* elements =
3146 [[NSMutableArray alloc] init];
3147 if ([result isKindOfClass:[NSArray class]]) {
3148 for (NSArray* elementArray in result) {
3149 [elements addObject:elementArray[0]];
3152 valueWithCGRect:CGRectMake(
3153 [elementArray[1] floatValue],
3154 [elementArray[2] floatValue],
3155 [elementArray[3] floatValue],
3156 [elementArray[4] floatValue])]
3157 forKey:elementArray[0]];
3160 completion(elements);
3164 #pragma mark - Methods related to Scribble support
3168 if (@available(iOS 14.0, *)) {
3170 if (parentView != nil) {
3171 UIIndirectScribbleInteraction* scribbleInteraction = [[UIIndirectScribbleInteraction alloc]
3172 initWithDelegate:(id<UIIndirectScribbleInteractionDelegate>)self];
3173 [parentView addInteraction:scribbleInteraction];
3180 - (void)resetViewResponder {
3181 _viewResponder = nil;
3185 #pragma mark FlutterKeySecondaryResponder
3200 - (id)flutterFirstResponder {
3201 if (
self.isFirstResponder) {
3204 for (UIView* subView in
self.subviews) {
3205 UIView* firstResponder = subView.flutterFirstResponder;
3206 if (firstResponder) {
3207 return firstResponder;
void(^ FlutterResult)(id _Nullable result)
FLUTTER_DARWIN_EXPORT NSObject const * FlutterMethodNotImplemented
CGRect localRectFromFrameworkTransform
void resetScribbleInteractionStatusIfEnding
UITextRange * markedTextRange
id< UITextInputDelegate > inputDelegate
API_AVAILABLE(ios(13.0)) @interface FlutterTextPlaceholder UITextRange * selectedTextRange
instancetype initWithOwner
id< FlutterViewResponder > viewResponder
UITextSmartQuotesType smartQuotesType API_AVAILABLE(ios(11.0))
UIReturnKeyType returnKeyType
CGRect caretRectForPosition
UIKeyboardAppearance keyboardAppearance
static UITextAutocapitalizationType ToUITextAutoCapitalizationType(NSDictionary *type)
static NSString *const kSetMarkedTextRectMethod
static NSString *const kFinishAutofillContextMethod
bool _isFloatingCursorActive
static BOOL IsEmoji(NSString *text, NSRange charRange)
static NSString *const kAutofillHints
static NSString *const kAutofillEditingValue
FlutterTextRange * _selectedTextRange
static NSString *const kSecureTextEntry
bool _enableInteractiveSelection
static BOOL ShouldShowSystemKeyboard(NSDictionary *type)
static NSString *const kShowMethod
static NSString *const kUpdateConfigMethod
static UIKeyboardType ToUIKeyboardType(NSDictionary *type)
CGPoint _floatingCursorOffset
static NSString *const kSmartDashesType
static FLUTTER_ASSERT_ARC const char kTextAffinityDownstream[]
static constexpr double kUITextInputAccessibilityEnablingDelaySeconds
static NSString *const kSetSelectionRectsMethod
typedef NS_ENUM(NSInteger, FlutterAutofillType)
static NSString *const kAutocorrectionType
static NSString *const kSetPlatformViewClientMethod
static NSString *const kAssociatedAutofillFields
static NSString *const kKeyboardType
static const NSTimeInterval kKeyboardAnimationDelaySeconds
static NSString *const kSetEditingStateMethod
UIInputViewController * _inputViewController
static NSString *const kAutofillId
static NSString *const kOnInteractiveKeyboardPointerUpMethod
bool _isSystemKeyboardEnabled
static NSString *const kClearClientMethod
static NSString *const kKeyboardAppearance
const CGRect kInvalidFirstRect
static FlutterAutofillType AutofillTypeOf(NSDictionary *configuration)
static NSString *const kSmartQuotesType
static NSString *const kEnableDeltaModel
FlutterScribbleInteractionStatus _scribbleInteractionStatus
static NSString *const kSetClientMethod
static NSString *const kEnableInteractiveSelection
static NSString *const kInputAction
static NSString *const kStartLiveTextInputMethod
static NSString *const kAutofillProperties
static BOOL IsFieldPasswordRelated(NSDictionary *configuration)
static const NSTimeInterval kKeyboardAnimationTimeToCompleteion
static BOOL IsApproximatelyEqual(float x, float y, float delta)
static NSString *const kDeprecatedSetSelectionRectsMethod
static const char kTextAffinityUpstream[]
static NSString * AutofillIdFromDictionary(NSDictionary *dictionary)
static UIReturnKeyType ToUIReturnKeyType(NSString *inputType)
static BOOL IsSelectionRectBoundaryCloserToPoint(CGPoint point, CGRect selectionRect, BOOL selectionRectIsRTL, BOOL useTrailingBoundaryOfSelectionRect, CGRect otherSelectionRect, BOOL otherSelectionRectIsRTL, CGFloat verticalPrecision)
static NSString *const kHideMethod
static UITextContentType ToUITextContentType(NSArray< NSString * > *hints)
static NSString *const kSetEditableSizeAndTransformMethod
static NSString *const kOnInteractiveKeyboardPointerMoveMethod
const char * _selectionAffinity
FlutterTextInputPlugin * textInputPlugin
UIApplication * application
instancetype positionWithIndex:(NSUInteger index)
instancetype positionWithIndex:affinity:(NSUInteger index,[affinity] UITextStorageDirection affinity)
UITextStorageDirection affinity
instancetype rangeWithNSRange:(NSRange range)
NSWritingDirection writingDirection
instancetype selectionRectWithRect:position:writingDirection:(CGRect rect,[position] NSUInteger position,[writingDirection] NSWritingDirection writingDirection)
instancetype selectionRectWithRectAndInfo:position:writingDirection:containsStart:containsEnd:isVertical:(CGRect rect,[position] NSUInteger position,[writingDirection] NSWritingDirection writingDirection,[containsStart] BOOL containsStart,[containsEnd] BOOL containsEnd,[isVertical] BOOL isVertical)
FlutterTextInputPlugin * target