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"
15 #import "flutter/shell/platform/darwin/common/InternalFlutterSwiftCommon/InternalFlutterSwiftCommon.h"
39 #pragma mark - TextInput channel method names.
48 @"TextInput.setEditableSizeAndTransform";
59 @"TextInput.onPointerMoveForInteractiveKeyboard";
61 @"TextInput.onPointerUpForInteractiveKeyboard";
63 #pragma mark - TextInputConfiguration Field Names
85 #pragma mark - Static Functions
88 static BOOL
IsEmoji(NSString* text, NSRange charRange) {
90 BOOL gotCodePoint = [text getBytes:&codePoint
91 maxLength:
sizeof(codePoint)
93 encoding:NSUTF32StringEncoding
97 return gotCodePoint && u_hasBinaryProperty(codePoint, UCHAR_EMOJI);
105 NSString* inputType = type[
@"name"];
106 return ![inputType isEqualToString:
@"TextInputType.none"];
109 NSString* inputType = type[
@"name"];
110 if ([inputType isEqualToString:
@"TextInputType.address"]) {
111 return UIKeyboardTypeDefault;
113 if ([inputType isEqualToString:
@"TextInputType.datetime"]) {
114 return UIKeyboardTypeNumbersAndPunctuation;
116 if ([inputType isEqualToString:
@"TextInputType.emailAddress"]) {
117 return UIKeyboardTypeEmailAddress;
119 if ([inputType isEqualToString:
@"TextInputType.multiline"]) {
120 return UIKeyboardTypeDefault;
122 if ([inputType isEqualToString:
@"TextInputType.name"]) {
123 return UIKeyboardTypeNamePhonePad;
125 if ([inputType isEqualToString:
@"TextInputType.number"]) {
126 if ([type[
@"signed"] boolValue]) {
127 return UIKeyboardTypeNumbersAndPunctuation;
129 if ([type[
@"decimal"] boolValue]) {
130 return UIKeyboardTypeDecimalPad;
132 return UIKeyboardTypeNumberPad;
134 if ([inputType isEqualToString:
@"TextInputType.phone"]) {
135 return UIKeyboardTypePhonePad;
137 if ([inputType isEqualToString:
@"TextInputType.text"]) {
138 return UIKeyboardTypeDefault;
140 if ([inputType isEqualToString:
@"TextInputType.url"]) {
141 return UIKeyboardTypeURL;
143 if ([inputType isEqualToString:
@"TextInputType.visiblePassword"]) {
144 return UIKeyboardTypeASCIICapable;
146 if ([inputType isEqualToString:
@"TextInputType.webSearch"]) {
147 return UIKeyboardTypeWebSearch;
149 if ([inputType isEqualToString:
@"TextInputType.twitter"]) {
150 return UIKeyboardTypeTwitter;
152 return UIKeyboardTypeDefault;
156 NSString* textCapitalization = type[
@"textCapitalization"];
157 if ([textCapitalization isEqualToString:
@"TextCapitalization.characters"]) {
158 return UITextAutocapitalizationTypeAllCharacters;
159 }
else if ([textCapitalization isEqualToString:
@"TextCapitalization.sentences"]) {
160 return UITextAutocapitalizationTypeSentences;
161 }
else if ([textCapitalization isEqualToString:
@"TextCapitalization.words"]) {
162 return UITextAutocapitalizationTypeWords;
164 return UITextAutocapitalizationTypeNone;
172 if ([inputType isEqualToString:
@"TextInputAction.unspecified"]) {
173 return UIReturnKeyDefault;
176 if ([inputType isEqualToString:
@"TextInputAction.done"]) {
177 return UIReturnKeyDone;
180 if ([inputType isEqualToString:
@"TextInputAction.go"]) {
181 return UIReturnKeyGo;
184 if ([inputType isEqualToString:
@"TextInputAction.send"]) {
185 return UIReturnKeySend;
188 if ([inputType isEqualToString:
@"TextInputAction.search"]) {
189 return UIReturnKeySearch;
192 if ([inputType isEqualToString:
@"TextInputAction.next"]) {
193 return UIReturnKeyNext;
196 if ([inputType isEqualToString:
@"TextInputAction.continueAction"]) {
197 return UIReturnKeyContinue;
200 if ([inputType isEqualToString:
@"TextInputAction.join"]) {
201 return UIReturnKeyJoin;
204 if ([inputType isEqualToString:
@"TextInputAction.route"]) {
205 return UIReturnKeyRoute;
208 if ([inputType isEqualToString:
@"TextInputAction.emergencyCall"]) {
209 return UIReturnKeyEmergencyCall;
212 if ([inputType isEqualToString:
@"TextInputAction.newline"]) {
213 return UIReturnKeyDefault;
217 return UIReturnKeyDefault;
221 if (!hints || hints.count == 0) {
226 NSString* hint = hints[0];
227 if ([hint isEqualToString:
@"addressCityAndState"]) {
228 return UITextContentTypeAddressCityAndState;
231 if ([hint isEqualToString:
@"addressState"]) {
232 return UITextContentTypeAddressState;
235 if ([hint isEqualToString:
@"addressCity"]) {
236 return UITextContentTypeAddressCity;
239 if ([hint isEqualToString:
@"sublocality"]) {
240 return UITextContentTypeSublocality;
243 if ([hint isEqualToString:
@"streetAddressLine1"]) {
244 return UITextContentTypeStreetAddressLine1;
247 if ([hint isEqualToString:
@"streetAddressLine2"]) {
248 return UITextContentTypeStreetAddressLine2;
251 if ([hint isEqualToString:
@"countryName"]) {
252 return UITextContentTypeCountryName;
255 if ([hint isEqualToString:
@"fullStreetAddress"]) {
256 return UITextContentTypeFullStreetAddress;
259 if ([hint isEqualToString:
@"postalCode"]) {
260 return UITextContentTypePostalCode;
263 if ([hint isEqualToString:
@"location"]) {
264 return UITextContentTypeLocation;
267 if ([hint isEqualToString:
@"creditCardNumber"]) {
268 return UITextContentTypeCreditCardNumber;
271 if ([hint isEqualToString:
@"email"]) {
272 return UITextContentTypeEmailAddress;
275 if ([hint isEqualToString:
@"jobTitle"]) {
276 return UITextContentTypeJobTitle;
279 if ([hint isEqualToString:
@"givenName"]) {
280 return UITextContentTypeGivenName;
283 if ([hint isEqualToString:
@"middleName"]) {
284 return UITextContentTypeMiddleName;
287 if ([hint isEqualToString:
@"familyName"]) {
288 return UITextContentTypeFamilyName;
291 if ([hint isEqualToString:
@"name"]) {
292 return UITextContentTypeName;
295 if ([hint isEqualToString:
@"namePrefix"]) {
296 return UITextContentTypeNamePrefix;
299 if ([hint isEqualToString:
@"nameSuffix"]) {
300 return UITextContentTypeNameSuffix;
303 if ([hint isEqualToString:
@"nickname"]) {
304 return UITextContentTypeNickname;
307 if ([hint isEqualToString:
@"organizationName"]) {
308 return UITextContentTypeOrganizationName;
311 if ([hint isEqualToString:
@"telephoneNumber"]) {
312 return UITextContentTypeTelephoneNumber;
315 if ([hint isEqualToString:
@"password"]) {
316 return UITextContentTypePassword;
319 if ([hint isEqualToString:
@"oneTimeCode"]) {
320 return UITextContentTypeOneTimeCode;
323 if ([hint isEqualToString:
@"newPassword"]) {
324 return UITextContentTypeNewPassword;
392 typedef NS_ENUM(NSInteger, FlutterAutofillType) {
396 kFlutterAutofillTypeNone,
397 kFlutterAutofillTypeRegular,
398 kFlutterAutofillTypePassword,
408 if (isSecureTextEntry) {
415 if ([contentType isEqualToString:UITextContentTypePassword] ||
416 [contentType isEqualToString:UITextContentTypeUsername]) {
420 if ([contentType isEqualToString:UITextContentTypeNewPassword]) {
430 return kFlutterAutofillTypePassword;
435 return kFlutterAutofillTypePassword;
440 return !autofill || [contentType isEqualToString:
@""] ? kFlutterAutofillTypeNone
441 : kFlutterAutofillTypeRegular;
445 return fabsf(x - y) <= delta;
471 CGRect selectionRect,
472 BOOL selectionRectIsRTL,
473 BOOL useTrailingBoundaryOfSelectionRect,
474 CGRect otherSelectionRect,
475 BOOL otherSelectionRectIsRTL,
476 CGFloat verticalPrecision) {
478 if (CGRectContainsPoint(
480 selectionRect.origin.x + ((useTrailingBoundaryOfSelectionRect ^ selectionRectIsRTL)
481 ? 0.5 * selectionRect.size.width
483 selectionRect.origin.y, 0.5 * selectionRect.size.width, selectionRect.size.height),
488 CGPoint pointForSelectionRect = CGPointMake(
489 selectionRect.origin.x +
490 (selectionRectIsRTL ^ useTrailingBoundaryOfSelectionRect ? selectionRect.size.width : 0),
491 selectionRect.origin.y + selectionRect.size.height * 0.5);
492 float yDist = fabs(pointForSelectionRect.y - point.y);
493 float xDist = fabs(pointForSelectionRect.x - point.x);
496 CGPoint pointForOtherSelectionRect = CGPointMake(
497 otherSelectionRect.origin.x + (otherSelectionRectIsRTL ? otherSelectionRect.size.width : 0),
498 otherSelectionRect.origin.y + otherSelectionRect.size.height * 0.5);
499 float yDistOther = fabs(pointForOtherSelectionRect.y - point.y);
500 float xDistOther = fabs(pointForOtherSelectionRect.x - point.x);
505 BOOL isCloserVertically = yDist < yDistOther - verticalPrecision;
507 BOOL isAboveBottomOfLine = point.y <= selectionRect.origin.y + selectionRect.size.height;
508 BOOL isCloserHorizontally = xDist < xDistOther;
509 BOOL isBelowBottomOfLine = point.y > selectionRect.origin.y + selectionRect.size.height;
512 if (selectionRectIsRTL) {
513 isFarther = selectionRect.origin.x < otherSelectionRect.origin.x;
515 isFarther = selectionRect.origin.x +
516 (useTrailingBoundaryOfSelectionRect ? selectionRect.size.width : 0) >
517 otherSelectionRect.origin.x;
519 return (isCloserVertically ||
520 (isEqualVertically &&
521 ((isAboveBottomOfLine && isCloserHorizontally) || (isBelowBottomOfLine && isFarther))));
524 #pragma mark - FlutterTextPosition
528 + (instancetype)positionWithIndex:(NSUInteger)index {
529 return [[
FlutterTextPosition alloc] initWithIndex:index affinity:UITextStorageDirectionForward];
532 + (instancetype)positionWithIndex:(NSUInteger)index affinity:(UITextStorageDirection)affinity {
536 - (instancetype)initWithIndex:(NSUInteger)index affinity:(UITextStorageDirection)affinity {
547 #pragma mark - FlutterTextRange
551 + (instancetype)rangeWithNSRange:(NSRange)range {
555 - (instancetype)initWithNSRange:(NSRange)range {
563 - (UITextPosition*)start {
565 affinity:UITextStorageDirectionForward];
568 - (UITextPosition*)end {
570 affinity:UITextStorageDirectionBackward];
574 return self.range.length == 0;
577 - (id)copyWithZone:(NSZone*)zone {
582 return NSEqualRanges(
self.
range, other.
range);
586 #pragma mark - FlutterTokenizer
596 - (instancetype)initWithTextInput:(UIResponder<UITextInput>*)textInput {
598 @"The FlutterTokenizer can only be used in a FlutterTextInputView");
599 self = [
super initWithTextInput:textInput];
606 - (UITextRange*)rangeEnclosingPosition:(UITextPosition*)position
607 withGranularity:(UITextGranularity)granularity
608 inDirection:(UITextDirection)direction {
610 switch (granularity) {
611 case UITextGranularityLine:
614 result = [
self lineEnclosingPosition:position inDirection:direction];
616 case UITextGranularityCharacter:
617 case UITextGranularityWord:
618 case UITextGranularitySentence:
619 case UITextGranularityParagraph:
620 case UITextGranularityDocument:
622 result = [
super rangeEnclosingPosition:position
623 withGranularity:granularity
624 inDirection:direction];
630 - (UITextRange*)lineEnclosingPosition:(UITextPosition*)position
631 inDirection:(UITextDirection)direction {
633 if (@available(iOS 17.0, *)) {
638 if (flutterPosition.
index > _textInputView.text.length ||
639 (flutterPosition.
index == _textInputView.text.length &&
640 direction == UITextStorageDirectionForward)) {
646 NSString* textAfter = [_textInputView
647 textInRange:[_textInputView textRangeFromPosition:position
648 toPosition:[_textInputView endOfDocument]]];
649 NSArray<NSString*>* linesAfter = [textAfter componentsSeparatedByString:@"\n"];
650 NSInteger offSetToLineBreak = [linesAfter firstObject].length;
651 UITextPosition* lineBreakAfter = [_textInputView positionFromPosition:position
652 offset:offSetToLineBreak];
654 NSString* textBefore = [_textInputView
655 textInRange:[_textInputView textRangeFromPosition:[_textInputView beginningOfDocument]
656 toPosition:position]];
657 NSArray<NSString*>* linesBefore = [textBefore componentsSeparatedByString:@"\n"];
658 NSInteger offSetFromLineBreak = [linesBefore lastObject].length;
659 UITextPosition* lineBreakBefore = [_textInputView positionFromPosition:position
660 offset:-offSetFromLineBreak];
662 return [_textInputView textRangeFromPosition:lineBreakBefore toPosition:lineBreakAfter];
667 #pragma mark - FlutterTextSelectionRect
672 @synthesize rect = _rect;
678 + (instancetype)selectionRectWithRectAndInfo:(CGRect)rect
679 position:(NSUInteger)position
680 writingDirection:(NSWritingDirection)writingDirection
681 containsStart:(BOOL)containsStart
682 containsEnd:(BOOL)containsEnd
683 isVertical:(BOOL)isVertical {
686 writingDirection:writingDirection
687 containsStart:containsStart
688 containsEnd:containsEnd
689 isVertical:isVertical];
692 + (instancetype)selectionRectWithRect:(CGRect)rect position:(NSUInteger)position {
695 writingDirection:NSWritingDirectionNatural
701 + (instancetype)selectionRectWithRect:(CGRect)rect
702 position:(NSUInteger)position
703 writingDirection:(NSWritingDirection)writingDirection {
706 writingDirection:writingDirection
712 - (instancetype)initWithRectAndInfo:(CGRect)rect
713 position:(NSUInteger)position
714 writingDirection:(NSWritingDirection)writingDirection
715 containsStart:(BOOL)containsStart
716 containsEnd:(BOOL)containsEnd
717 isVertical:(BOOL)isVertical {
731 return _writingDirection == NSWritingDirectionRightToLeft;
736 #pragma mark - FlutterTextPlaceholder
740 - (NSArray<UITextSelectionRect*>*)rects {
756 @property(nonatomic, retain, readonly) UITextField*
textField;
760 UITextField* _textField;
765 _textField = [[UITextField alloc] init];
770 - (BOOL)isKindOfClass:(Class)aClass {
771 return [
super isKindOfClass:aClass] || (aClass == [UITextField class]);
774 - (NSMethodSignature*)methodSignatureForSelector:(
SEL)aSelector {
775 NSMethodSignature* signature = [
super methodSignatureForSelector:aSelector];
777 signature = [
self.textField methodSignatureForSelector:aSelector];
782 - (void)forwardInvocation:(NSInvocation*)anInvocation {
783 [anInvocation invokeWithTarget:self.textField];
789 @property(nonatomic, readonly, weak) id<FlutterTextInputDelegate> textInputDelegate;
790 @property(nonatomic, readonly) UIView* hostView;
795 @property(nonatomic, copy) NSString* autofillId;
796 @property(nonatomic, readonly) CATransform3D editableTransform;
797 @property(nonatomic, assign) CGRect markedRect;
799 @property(nonatomic, assign) BOOL preventCursorDismissWhenResignFirstResponder;
800 @property(nonatomic) BOOL isVisibleToAutofill;
801 @property(nonatomic, assign) BOOL accessibilityEnabled;
802 @property(nonatomic, assign)
int textInputClient;
806 @property(nonatomic, copy) NSString* temporarilyDeletedComposedCharacter;
807 @property(nonatomic, assign) CGRect editMenuTargetRect;
808 @property(nonatomic, strong) NSArray<NSDictionary*>* editMenuItems;
810 - (void)setEditableTransform:(NSArray*)matrix;
814 int _textInputClient;
831 @synthesize tokenizer = _tokenizer;
834 self = [
super initWithFrame:CGRectZero];
837 _textInputClient = 0;
839 _preventCursorDismissWhenResignFirstResponder = NO;
842 _text = [[NSMutableString alloc] init];
847 _pendingDeltas = [[NSMutableArray alloc] init];
850 _editableTransform = CATransform3D();
853 _autocapitalizationType = UITextAutocapitalizationTypeSentences;
854 _autocorrectionType = UITextAutocorrectionTypeDefault;
855 _spellCheckingType = UITextSpellCheckingTypeDefault;
856 _enablesReturnKeyAutomatically = NO;
857 _keyboardAppearance = UIKeyboardAppearanceDefault;
858 _keyboardType = UIKeyboardTypeDefault;
859 _returnKeyType = UIReturnKeyDone;
860 _secureTextEntry = NO;
861 _enableDeltaModel = NO;
863 _accessibilityEnabled = NO;
864 _smartQuotesType = UITextSmartQuotesTypeYes;
865 _smartDashesType = UITextSmartDashesTypeYes;
866 _selectionRects = [[NSArray alloc] init];
867 if (@available(iOS 17.0, *)) {
868 _inlinePredictionType = UITextInlinePredictionTypeNo;
871 if (@available(iOS 14.0, *)) {
872 UIScribbleInteraction* interaction = [[UIScribbleInteraction alloc] initWithDelegate:self];
873 [
self addInteraction:interaction];
877 if (@available(iOS 16.0, *)) {
878 _editMenuInteraction = [[UIEditMenuInteraction alloc] initWithDelegate:self];
879 [
self addInteraction:_editMenuInteraction];
885 - (void)handleSearchWebAction {
886 [
self.textInputDelegate flutterTextInputView:self
887 searchWebWithSelectedText:[
self textInRange:_selectedTextRange]];
890 - (void)handleLookUpAction {
891 [
self.textInputDelegate flutterTextInputView:self
892 lookUpSelectedText:[
self textInRange:_selectedTextRange]];
895 - (void)handleShareAction {
896 [
self.textInputDelegate flutterTextInputView:self
897 shareSelectedText:[
self textInRange:_selectedTextRange]];
901 - (UICommand*)searchCommandWithSelector:(
SEL)selector
902 element:(UIMenuElement*)element API_AVAILABLE(ios(16.0)) {
903 if ([element isKindOfClass:UICommand.class]) {
904 UICommand* command = (UICommand*)element;
905 return command.action == selector ? command : nil;
906 }
else if ([element isKindOfClass:UIMenu.class]) {
907 NSArray<UIMenuElement*>* children = ((UIMenu*)element).children;
908 for (UIMenuElement* child in children) {
909 UICommand* result = [
self searchCommandWithSelector:selector element:child];
920 - (void)addBasicEditingCommandToItems:(NSMutableArray*)items
922 selector:(
SEL)selector
923 suggestedMenu:(UIMenu*)suggestedMenu {
924 UICommand* command = [
self searchCommandWithSelector:selector element:suggestedMenu];
926 [items addObject:command];
928 NSString* errorMessage =
929 [NSString stringWithFormat:@"Cannot find context menu item of type \"%@\".", type];
930 [FlutterLogger logError:errorMessage];
934 - (void)addAdditionalBasicCommandToItems:(NSMutableArray*)items
936 selector:(
SEL)selector
937 encodedItem:(NSDictionary<NSString*,
id>*)encodedItem {
938 NSString* title = encodedItem[@"title"];
940 UICommand* command = [UICommand commandWithTitle:title
944 [items addObject:command];
946 NSString* errorMessage =
947 [NSString stringWithFormat:@"Missing title for context menu item of type \"%@\".", type];
948 [FlutterLogger logError:errorMessage];
952 - (UIMenu*)editMenuInteraction:(UIEditMenuInteraction*)interaction
953 menuForConfiguration:(UIEditMenuConfiguration*)configuration
954 suggestedActions:(NSArray<UIMenuElement*>*)suggestedActions API_AVAILABLE(ios(16.0)) {
955 UIMenu* suggestedMenu = [UIMenu menuWithChildren:suggestedActions];
956 if (!_editMenuItems) {
957 return suggestedMenu;
960 NSMutableArray* items = [NSMutableArray array];
961 for (NSDictionary<NSString*, id>* encodedItem in _editMenuItems) {
962 NSString* type = encodedItem[@"type"];
963 if ([type isEqualToString:
@"copy"]) {
964 [
self addBasicEditingCommandToItems:items
966 selector:@selector(copy:)
967 suggestedMenu:suggestedMenu];
968 }
else if ([type isEqualToString:
@"paste"]) {
969 [
self addBasicEditingCommandToItems:items
971 selector:@selector(paste:)
972 suggestedMenu:suggestedMenu];
973 }
else if ([type isEqualToString:
@"cut"]) {
974 [
self addBasicEditingCommandToItems:items
976 selector:@selector(cut:)
977 suggestedMenu:suggestedMenu];
978 }
else if ([type isEqualToString:
@"delete"]) {
979 [
self addBasicEditingCommandToItems:items
981 selector:@selector(delete:)
982 suggestedMenu:suggestedMenu];
983 }
else if ([type isEqualToString:
@"selectAll"]) {
984 [
self addBasicEditingCommandToItems:items
986 selector:@selector(selectAll:)
987 suggestedMenu:suggestedMenu];
988 }
else if ([type isEqualToString:
@"searchWeb"]) {
989 [
self addAdditionalBasicCommandToItems:items
991 selector:@selector(handleSearchWebAction)
992 encodedItem:encodedItem];
993 }
else if ([type isEqualToString:
@"share"]) {
994 [
self addAdditionalBasicCommandToItems:items
996 selector:@selector(handleShareAction)
997 encodedItem:encodedItem];
998 }
else if ([type isEqualToString:
@"lookUp"]) {
999 [
self addAdditionalBasicCommandToItems:items
1001 selector:@selector(handleLookUpAction)
1002 encodedItem:encodedItem];
1003 }
else if ([type isEqualToString:
@"captureTextFromCamera"]) {
1004 if (@available(iOS 15.0, *)) {
1005 [
self addBasicEditingCommandToItems:items
1007 selector:@selector(captureTextFromCamera:)
1008 suggestedMenu:suggestedMenu];
1010 }
else if ([type isEqualToString:
@"custom"]) {
1011 NSString* callbackId = encodedItem[@"id"];
1012 NSString* title = encodedItem[@"title"];
1013 if (callbackId && title) {
1015 UIAction* action = [UIAction
1016 actionWithTitle:title
1019 handler:^(__kindof UIAction* _Nonnull action) {
1022 [strongSelf.textInputDelegate flutterTextInputView:strongSelf
1023 performContextMenuCustomActionWithActionID:callbackId
1024 textInputClient:strongSelf->
1028 [items addObject:action];
1032 return [UIMenu menuWithChildren:items];
1035 - (void)editMenuInteraction:(UIEditMenuInteraction*)interaction
1036 willDismissMenuForConfiguration:(UIEditMenuConfiguration*)configuration
1037 animator:(
id<UIEditMenuInteractionAnimating>)animator
1038 API_AVAILABLE(ios(16.0)) {
1039 [
self.textInputDelegate flutterTextInputView:self
1040 willDismissEditMenuWithTextInputClient:_textInputClient];
1043 - (CGRect)editMenuInteraction:(UIEditMenuInteraction*)interaction
1044 targetRectForConfiguration:(UIEditMenuConfiguration*)configuration API_AVAILABLE(ios(16.0)) {
1045 return _editMenuTargetRect;
1048 - (void)showEditMenuWithTargetRect:(CGRect)targetRect
1049 items:(NSArray<NSDictionary*>*)items API_AVAILABLE(ios(16.0)) {
1050 _editMenuTargetRect = targetRect;
1051 _editMenuItems = items;
1053 UIEditMenuConfiguration* config =
1054 [UIEditMenuConfiguration configurationWithIdentifier:nil sourcePoint:CGPointZero];
1055 [
self.editMenuInteraction presentEditMenuWithConfiguration:config];
1059 [
self.editMenuInteraction dismissMenu];
1062 - (void)configureWithDictionary:(NSDictionary*)configuration {
1063 NSDictionary* inputType = configuration[kKeyboardType];
1065 NSDictionary* autofill = configuration[kAutofillProperties];
1067 self.secureTextEntry = [configuration[kSecureTextEntry] boolValue];
1068 self.enableDeltaModel = [configuration[kEnableDeltaModel] boolValue];
1075 NSString* smartDashesType = configuration[kSmartDashesType];
1077 bool smartDashesIsDisabled = smartDashesType && [smartDashesType isEqualToString:@"0"];
1078 self.smartDashesType = smartDashesIsDisabled ? UITextSmartDashesTypeNo : UITextSmartDashesTypeYes;
1079 NSString* smartQuotesType = configuration[kSmartQuotesType];
1081 bool smartQuotesIsDisabled = smartQuotesType && [smartQuotesType isEqualToString:@"0"];
1082 self.smartQuotesType = smartQuotesIsDisabled ? UITextSmartQuotesTypeNo : UITextSmartQuotesTypeYes;
1084 self.keyboardAppearance = UIKeyboardAppearanceDark;
1086 self.keyboardAppearance = UIKeyboardAppearanceLight;
1088 self.keyboardAppearance = UIKeyboardAppearanceDefault;
1090 NSString* autocorrect = configuration[kAutocorrectionType];
1091 bool autocorrectIsDisabled = autocorrect && ![autocorrect boolValue];
1092 self.autocorrectionType =
1093 autocorrectIsDisabled ? UITextAutocorrectionTypeNo : UITextAutocorrectionTypeDefault;
1094 self.spellCheckingType =
1095 autocorrectIsDisabled ? UITextSpellCheckingTypeNo : UITextSpellCheckingTypeDefault;
1097 if (autofill == nil) {
1098 self.textContentType =
@"";
1101 [
self setTextInputState:autofill[kAutofillEditingValue]];
1102 NSAssert(_autofillId,
@"The autofill configuration must contain an autofill id");
1106 self.isVisibleToAutofill = autofill || _secureTextEntry;
1108 if (@available(iOS 17.0, *)) {
1111 id enableInlinePrediction = configuration[kEnableInlinePrediction];
1112 BOOL enabled = enableInlinePrediction != nil && enableInlinePrediction != [NSNull null] &&
1113 [enableInlinePrediction boolValue];
1114 self.inlinePredictionType =
1115 enabled ? UITextInlinePredictionTypeYes : UITextInlinePredictionTypeNo;
1119 - (UITextContentType)textContentType {
1120 return _textContentType;
1133 - (UIColor*)insertionPointColor {
1134 return [UIColor clearColor];
1137 - (UIColor*)selectionBarColor {
1138 return [UIColor clearColor];
1141 - (UIColor*)selectionHighlightColor {
1142 return [UIColor clearColor];
1145 - (UIInputViewController*)inputViewController {
1157 return _textInputPlugin.textInputDelegate;
1160 - (BOOL)respondsToSelector:(
SEL)selector {
1161 if (@available(iOS 17.0, *)) {
1163 if (selector ==
@selector(insertionPointColor)) {
1167 return [
super respondsToSelector:selector];
1170 - (void)setTextInputClient:(
int)client {
1171 _textInputClient = client;
1175 - (UITextInteraction*)textInteraction
API_AVAILABLE(ios(13.0)) {
1176 if (!_textInteraction) {
1177 _textInteraction = [UITextInteraction textInteractionForMode:UITextInteractionModeEditable];
1178 _textInteraction.textInput =
self;
1180 return _textInteraction;
1183 - (void)setTextInputState:(NSDictionary*)state {
1190 [
self addInteraction:self.textInteraction];
1193 NSString* newText = state[@"text"];
1194 BOOL textChanged = ![
self.text isEqualToString:newText];
1196 [
self.inputDelegate textWillChange:self];
1197 [
self.text setString:newText];
1199 NSInteger composingBase = [state[@"composingBase"] intValue];
1200 NSInteger composingExtent = [state[@"composingExtent"] intValue];
1201 NSRange composingRange = [
self clampSelection:NSMakeRange(MIN(composingBase, composingExtent),
1202 ABS(composingBase - composingExtent))
1205 self.markedTextRange =
1208 NSRange selectedRange = [
self clampSelectionFromBase:[state[@"selectionBase"] intValue]
1209 extent:[state[@"selectionExtent"] intValue]
1212 NSRange oldSelectedRange = [(
FlutterTextRange*)
self.selectedTextRange range];
1213 if (!NSEqualRanges(selectedRange, oldSelectedRange)) {
1214 [
self.inputDelegate selectionWillChange:self];
1222 [
self.inputDelegate selectionDidChange:self];
1226 [
self.inputDelegate textDidChange:self];
1229 if (_textInteraction) {
1230 [
self removeInteraction:_textInteraction];
1235 - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
1236 _scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
1237 [
self resetScribbleInteractionStatusIfEnding];
1238 [
self.viewResponder touchesBegan:touches withEvent:event];
1241 - (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {
1242 [
self.viewResponder touchesMoved:touches withEvent:event];
1245 - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
1246 [
self.viewResponder touchesEnded:touches withEvent:event];
1249 - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
1250 [
self.viewResponder touchesCancelled:touches withEvent:event];
1253 - (void)touchesEstimatedPropertiesUpdated:(NSSet*)touches {
1254 [
self.viewResponder touchesEstimatedPropertiesUpdated:touches];
1264 - (NSRange)clampSelectionFromBase:(
int)selectionBase
1265 extent:(
int)selectionExtent
1266 forText:(NSString*)text {
1267 int loc = MIN(selectionBase, selectionExtent);
1268 int len = ABS(selectionExtent - selectionBase);
1269 return loc < 0 ? NSMakeRange(0, 0)
1270 : [
self clampSelection:NSMakeRange(loc, len) forText:
self.text];
1273 - (NSRange)clampSelection:(NSRange)range forText:(NSString*)text {
1274 NSUInteger start = MIN(MAX(range.location, 0), text.length);
1275 NSUInteger length = MIN(range.length, text.length - start);
1276 return NSMakeRange(start, length);
1279 - (BOOL)isVisibleToAutofill {
1280 return self.frame.size.width > 0 &&
self.frame.size.height > 0;
1288 - (void)setIsVisibleToAutofill:(BOOL)isVisibleToAutofill {
1291 self.frame = isVisibleToAutofill ? CGRectMake(0, 0, 1, 1) : CGRectZero;
1294 #pragma mark UIScribbleInteractionDelegate
1299 if (@available(iOS 14.0, *)) {
1300 if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
1307 - (void)scribbleInteractionWillBeginWriting:(UIScribbleInteraction*)interaction
1308 API_AVAILABLE(ios(14.0)) {
1310 [
self.textInputDelegate flutterTextInputViewScribbleInteractionBegan:self];
1313 - (void)scribbleInteractionDidFinishWriting:(UIScribbleInteraction*)interaction
1314 API_AVAILABLE(ios(14.0)) {
1316 [
self.textInputDelegate flutterTextInputViewScribbleInteractionFinished:self];
1319 - (BOOL)scribbleInteraction:(UIScribbleInteraction*)interaction
1320 shouldBeginAtLocation:(CGPoint)location API_AVAILABLE(ios(14.0)) {
1324 - (BOOL)scribbleInteractionShouldDelayFocus:(UIScribbleInteraction*)interaction
1325 API_AVAILABLE(ios(14.0)) {
1329 #pragma mark - UIResponder Overrides
1331 - (BOOL)canBecomeFirstResponder {
1336 return _textInputClient != 0;
1339 - (BOOL)resignFirstResponder {
1340 BOOL success = [
super resignFirstResponder];
1342 if (!_preventCursorDismissWhenResignFirstResponder) {
1343 [
self.textInputDelegate flutterTextInputView:self
1344 didResignFirstResponderWithTextInputClient:_textInputClient];
1350 - (BOOL)canPerformAction:(
SEL)action withSender:(
id)sender {
1351 if (action ==
@selector(paste:)) {
1353 return [UIPasteboard generalPasteboard].hasStrings;
1354 }
else if (action ==
@selector(copy:) || action ==
@selector(cut:) ||
1355 action ==
@selector(
delete:)) {
1356 return [
self textInRange:_selectedTextRange].length > 0;
1357 }
else if (action ==
@selector(selectAll:)) {
1358 return self.hasText;
1359 }
else if (action ==
@selector(captureTextFromCamera:)) {
1360 if (@available(iOS 15.0, *)) {
1365 return [
super canPerformAction:action withSender:sender];
1368 #pragma mark - UIResponderStandardEditActions Overrides
1370 - (void)cut:(
id)sender {
1371 [UIPasteboard generalPasteboard].string = [
self textInRange:_selectedTextRange];
1372 [
self replaceRange:_selectedTextRange withText:@""];
1375 - (void)copy:(
id)sender {
1376 [UIPasteboard generalPasteboard].string = [
self textInRange:_selectedTextRange];
1379 - (void)paste:(
id)sender {
1380 NSString* pasteboardString = [UIPasteboard generalPasteboard].string;
1381 if (pasteboardString != nil) {
1382 [
self insertText:pasteboardString];
1386 - (void)delete:(
id)sender {
1387 [
self replaceRange:_selectedTextRange withText:@""];
1390 - (void)selectAll:(
id)sender {
1391 [
self setSelectedTextRange:[
self textRangeFromPosition:[
self beginningOfDocument]
1392 toPosition:[
self endOfDocument]]];
1395 #pragma mark - UITextInput Overrides
1397 - (id<UITextInputTokenizer>)tokenizer {
1398 if (_tokenizer == nil) {
1405 return [_selectedTextRange copy];
1409 - (void)setSelectedTextRangeLocal:(UITextRange*)selectedTextRange {
1414 rangeWithNSRange:fml::RangeForCharactersInRange(self.text, flutterTextRange.range)] copy];
1421 - (void)setSelectedTextRange:(UITextRange*)selectedTextRange {
1426 [
self setSelectedTextRangeLocal:selectedTextRange];
1428 if (_enableDeltaModel) {
1429 [
self updateEditingStateWithDelta:flutter::TextEditingDelta([
self.text UTF8String])];
1431 [
self updateEditingState];
1435 _scribbleFocusStatus == FlutterScribbleFocusStatusFocused) {
1439 if (flutterTextRange.
range.length > 0) {
1440 [
self.textInputDelegate flutterTextInputView:self showToolbar:_textInputClient];
1444 [
self resetScribbleInteractionStatusIfEnding];
1447 - (id)insertDictationResultPlaceholder {
1451 - (void)removeDictationResultPlaceholder:(
id)placeholder willInsertResult:(BOOL)willInsertResult {
1454 - (NSString*)textInRange:(UITextRange*)range {
1459 @"Expected a FlutterTextRange for range (got %@).", [range
class]);
1461 if (textRange.location == NSNotFound) {
1470 NSUInteger location = MIN(textRange.location,
self.text.length);
1471 NSUInteger length = MIN(
self.text.length - location, textRange.length);
1472 NSRange safeRange = NSMakeRange(location, length);
1473 return [
self.text substringWithRange:safeRange];
1478 - (void)replaceRangeLocal:(NSRange)range withText:(NSString*)text {
1479 [
self.text replaceCharactersInRange:[
self clampSelection:range forText:self.text]
1485 const NSRange newSelectionRange =
1486 [
self clampSelection:NSMakeRange(range.location + text.length, 0) forText:self.text];
1489 self.markedTextRange = nil;
1492 - (void)replaceRange:(UITextRange*)range withText:(NSString*)text {
1493 NSString* textBeforeChange = [
self.text copy];
1495 [
self replaceRangeLocal:replaceRange withText:text];
1496 if (_enableDeltaModel) {
1497 NSRange nextReplaceRange = [
self clampSelection:replaceRange forText:textBeforeChange];
1498 [
self updateEditingStateWithDelta:flutter::TextEditingDelta(
1499 [textBeforeChange UTF8String],
1501 nextReplaceRange.location,
1502 nextReplaceRange.location + nextReplaceRange.length),
1503 [text UTF8String])];
1505 [
self updateEditingState];
1509 - (BOOL)shouldChangeTextInRange:(UITextRange*)range replacementText:(NSString*)text {
1512 self.temporarilyDeletedComposedCharacter = nil;
1514 if (
self.
returnKeyType == UIReturnKeyDefault && [text isEqualToString:
@"\n"]) {
1515 [
self.textInputDelegate flutterTextInputView:self
1516 performAction:FlutterTextInputActionNewline
1517 withClient:_textInputClient];
1521 if ([text isEqualToString:
@"\n"]) {
1522 FlutterTextInputAction action;
1524 case UIReturnKeyDefault:
1525 action = FlutterTextInputActionUnspecified;
1527 case UIReturnKeyDone:
1528 action = FlutterTextInputActionDone;
1531 action = FlutterTextInputActionGo;
1533 case UIReturnKeySend:
1534 action = FlutterTextInputActionSend;
1536 case UIReturnKeySearch:
1537 case UIReturnKeyGoogle:
1538 case UIReturnKeyYahoo:
1539 action = FlutterTextInputActionSearch;
1541 case UIReturnKeyNext:
1542 action = FlutterTextInputActionNext;
1544 case UIReturnKeyContinue:
1545 action = FlutterTextInputActionContinue;
1547 case UIReturnKeyJoin:
1548 action = FlutterTextInputActionJoin;
1550 case UIReturnKeyRoute:
1551 action = FlutterTextInputActionRoute;
1553 case UIReturnKeyEmergencyCall:
1554 action = FlutterTextInputActionEmergencyCall;
1558 [
self.textInputDelegate flutterTextInputView:self
1559 performAction:action
1560 withClient:_textInputClient];
1569 - (void)setMarkedText:(NSString*)markedText selectedRange:(NSRange)markedSelectedRange {
1570 NSString* textBeforeChange = [
self.text copy];
1573 _scribbleFocusStatus != FlutterScribbleFocusStatusUnfocused) {
1577 if (markedText == nil) {
1582 const NSRange& actualReplacedRange = currentMarkedTextRange && !currentMarkedTextRange.isEmpty
1583 ? currentMarkedTextRange.
range
1587 [
self.text replaceCharactersInRange:actualReplacedRange withString:markedText];
1589 const NSRange newMarkedRange = NSMakeRange(actualReplacedRange.location, markedText.length);
1590 self.markedTextRange =
1593 [
self setSelectedTextRangeLocal:
1595 rangeWithNSRange:[
self clampSelection:NSMakeRange(markedSelectedRange.location +
1596 newMarkedRange.location,
1597 markedSelectedRange.length)
1598 forText:self.text]]];
1599 if (_enableDeltaModel) {
1600 NSRange nextReplaceRange = [
self clampSelection:actualReplacedRange forText:textBeforeChange];
1601 [
self updateEditingStateWithDelta:flutter::TextEditingDelta(
1602 [textBeforeChange UTF8String],
1604 nextReplaceRange.location,
1605 nextReplaceRange.location + nextReplaceRange.length),
1606 [markedText UTF8String])];
1608 [
self updateEditingState];
1614 - (void)setAttributedMarkedText:(NSAttributedString*)attributedString
1615 selectedRange:(NSRange)selectedRange API_AVAILABLE(ios(17.0)) {
1616 NSString* markedText = attributedString ? [attributedString string] :
@"";
1617 [
self setMarkedText:markedText selectedRange:selectedRange];
1620 - (void)unmarkText {
1624 self.markedTextRange = nil;
1625 if (_enableDeltaModel) {
1626 [
self updateEditingStateWithDelta:flutter::TextEditingDelta([
self.text UTF8String])];
1628 [
self updateEditingState];
1632 - (UITextRange*)textRangeFromPosition:(UITextPosition*)fromPosition
1633 toPosition:(UITextPosition*)toPosition {
1636 if (toIndex >= fromIndex) {
1649 - (NSUInteger)decrementOffsetPosition:(NSUInteger)position {
1650 return fml::RangeForCharacterAtIndex(
self.text, MAX(0, position - 1)).location;
1653 - (NSUInteger)incrementOffsetPosition:(NSUInteger)position {
1654 NSRange charRange = fml::RangeForCharacterAtIndex(
self.text, position);
1655 return MIN(position + charRange.length,
self.text.length);
1658 - (UITextPosition*)positionFromPosition:(UITextPosition*)position offset:(NSInteger)offset {
1661 NSInteger newLocation = (NSInteger)offsetPosition + offset;
1662 if (newLocation < 0 || newLocation > (NSInteger)
self.text.length) {
1671 for (NSInteger i = 0; i < offset && offsetPosition <
self.text.length; ++i) {
1672 offsetPosition = [
self incrementOffsetPosition:offsetPosition];
1675 for (NSInteger i = 0; i < ABS(offset) && offsetPosition > 0; ++i) {
1676 offsetPosition = [
self decrementOffsetPosition:offsetPosition];
1682 - (UITextPosition*)positionFromPosition:(UITextPosition*)position
1683 inDirection:(UITextLayoutDirection)direction
1684 offset:(NSInteger)offset {
1686 switch (direction) {
1687 case UITextLayoutDirectionLeft:
1688 case UITextLayoutDirectionUp:
1689 return [
self positionFromPosition:position offset:offset * -1];
1690 case UITextLayoutDirectionRight:
1691 case UITextLayoutDirectionDown:
1692 return [
self positionFromPosition:position offset:1];
1696 - (UITextPosition*)beginningOfDocument {
1700 - (UITextPosition*)endOfDocument {
1702 affinity:UITextStorageDirectionBackward];
1705 - (NSComparisonResult)comparePosition:(UITextPosition*)position toPosition:(UITextPosition*)other {
1708 if (positionIndex < otherIndex) {
1709 return NSOrderedAscending;
1711 if (positionIndex > otherIndex) {
1712 return NSOrderedDescending;
1716 if (positionAffinity == otherAffinity) {
1717 return NSOrderedSame;
1719 if (positionAffinity == UITextStorageDirectionBackward) {
1721 return NSOrderedAscending;
1724 return NSOrderedDescending;
1727 - (NSInteger)offsetFromPosition:(UITextPosition*)from toPosition:(UITextPosition*)toPosition {
1731 - (UITextPosition*)positionWithinRange:(UITextRange*)range
1732 farthestInDirection:(UITextLayoutDirection)direction {
1734 UITextStorageDirection affinity;
1735 switch (direction) {
1736 case UITextLayoutDirectionLeft:
1737 case UITextLayoutDirectionUp:
1739 affinity = UITextStorageDirectionForward;
1741 case UITextLayoutDirectionRight:
1742 case UITextLayoutDirectionDown:
1744 affinity = UITextStorageDirectionBackward;
1750 - (UITextRange*)characterRangeByExtendingPosition:(UITextPosition*)position
1751 inDirection:(UITextLayoutDirection)direction {
1753 NSUInteger startIndex;
1754 NSUInteger endIndex;
1755 switch (direction) {
1756 case UITextLayoutDirectionLeft:
1757 case UITextLayoutDirectionUp:
1758 startIndex = [
self decrementOffsetPosition:positionIndex];
1759 endIndex = positionIndex;
1761 case UITextLayoutDirectionRight:
1762 case UITextLayoutDirectionDown:
1763 startIndex = positionIndex;
1764 endIndex = [
self incrementOffsetPosition:positionIndex];
1770 #pragma mark - UITextInput text direction handling
1772 - (UITextWritingDirection)baseWritingDirectionForPosition:(UITextPosition*)position
1773 inDirection:(UITextStorageDirection)direction {
1775 return UITextWritingDirectionNatural;
1778 - (void)setBaseWritingDirection:(UITextWritingDirection)writingDirection
1779 forRange:(UITextRange*)range {
1783 #pragma mark - UITextInput cursor, selection rect handling
1785 - (void)setMarkedRect:(CGRect)markedRect {
1786 _markedRect = markedRect;
1793 - (void)setEditableTransform:(NSArray*)matrix {
1794 CATransform3D* transform = &_editableTransform;
1796 transform->m11 = [matrix[0] doubleValue];
1797 transform->m12 = [matrix[1] doubleValue];
1798 transform->m13 = [matrix[2] doubleValue];
1799 transform->m14 = [matrix[3] doubleValue];
1801 transform->m21 = [matrix[4] doubleValue];
1802 transform->m22 = [matrix[5] doubleValue];
1803 transform->m23 = [matrix[6] doubleValue];
1804 transform->m24 = [matrix[7] doubleValue];
1806 transform->m31 = [matrix[8] doubleValue];
1807 transform->m32 = [matrix[9] doubleValue];
1808 transform->m33 = [matrix[10] doubleValue];
1809 transform->m34 = [matrix[11] doubleValue];
1811 transform->m41 = [matrix[12] doubleValue];
1812 transform->m42 = [matrix[13] doubleValue];
1813 transform->m43 = [matrix[14] doubleValue];
1814 transform->m44 = [matrix[15] doubleValue];
1823 CGPoint points[] = {
1824 incomingRect.origin,
1825 CGPointMake(incomingRect.origin.x, incomingRect.origin.y + incomingRect.size.height),
1826 CGPointMake(incomingRect.origin.x + incomingRect.size.width, incomingRect.origin.y),
1827 CGPointMake(incomingRect.origin.x + incomingRect.size.width,
1828 incomingRect.origin.y + incomingRect.size.height)};
1830 CGPoint origin = CGPointMake(CGFLOAT_MAX, CGFLOAT_MAX);
1831 CGPoint farthest = CGPointMake(-CGFLOAT_MAX, -CGFLOAT_MAX);
1833 for (
int i = 0; i < 4; i++) {
1834 const CGPoint point = points[i];
1836 CGFloat x = _editableTransform.m11 * point.x + _editableTransform.m21 * point.y +
1837 _editableTransform.m41;
1838 CGFloat y = _editableTransform.m12 * point.x + _editableTransform.m22 * point.y +
1839 _editableTransform.m42;
1841 const CGFloat w = _editableTransform.m14 * point.x + _editableTransform.m24 * point.y +
1842 _editableTransform.m44;
1846 }
else if (w != 1.0) {
1851 origin.x = MIN(origin.x, x);
1852 origin.y = MIN(origin.y, y);
1853 farthest.x = MAX(farthest.x, x);
1854 farthest.y = MAX(farthest.y, y);
1856 return CGRectMake(origin.x, origin.y, farthest.x - origin.x, farthest.y - origin.y);
1865 - (CGRect)firstRectForRange:(UITextRange*)range {
1867 @"Expected a FlutterTextPosition for range.start (got %@).", [range.start
class]);
1869 @"Expected a FlutterTextPosition for range.end (got %@).", [range.end
class]);
1872 if (_markedTextRange != nil) {
1883 CGRect rect = _markedRect;
1884 if (CGRectIsEmpty(rect)) {
1885 rect = CGRectInset(rect, -0.1, 0);
1890 UIView* hostView = _textInputPlugin.hostView;
1891 NSAssert(hostView == nil || [
self isDescendantOfView:hostView],
@"%@ is not a descendant of %@",
1893 return hostView ? [hostView convertRect:_cachedFirstRect toView:self] :
_cachedFirstRect;
1897 _scribbleFocusStatus == FlutterScribbleFocusStatusUnfocused) {
1898 if (@available(iOS 17.0, *)) {
1908 [
self.textInputDelegate flutterTextInputView:self
1909 showAutocorrectionPromptRectForStart:start
1911 withClient:_textInputClient];
1919 if (@available(iOS 17, *)) {
1925 NSUInteger first = start;
1930 CGRect startSelectionRect = CGRectNull;
1931 CGRect endSelectionRect = CGRectNull;
1934 CGFloat minY = CGFLOAT_MAX;
1935 CGFloat maxY = CGFLOAT_MIN;
1938 rangeWithNSRange:fml::RangeForCharactersInRange(self.text, NSMakeRange(0, self.text.length))];
1939 for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
1940 BOOL startsOnOrBeforeStartOfRange = _selectionRects[i].position <= first;
1941 BOOL isLastSelectionRect = i + 1 == [_selectionRects count];
1942 BOOL endOfTextIsAfterStartOfRange = isLastSelectionRect && textRange.
range.length > first;
1943 BOOL nextSelectionRectIsAfterStartOfRange =
1944 !isLastSelectionRect && _selectionRects[i + 1].position > first;
1945 if (startsOnOrBeforeStartOfRange &&
1946 (endOfTextIsAfterStartOfRange || nextSelectionRectIsAfterStartOfRange)) {
1948 if (@available(iOS 17, *)) {
1949 startSelectionRect = _selectionRects[i].rect;
1951 return _selectionRects[i].rect;
1954 if (!CGRectIsNull(startSelectionRect)) {
1955 minY = fmin(minY, CGRectGetMinY(_selectionRects[i].rect));
1956 maxY = fmax(maxY, CGRectGetMaxY(_selectionRects[i].rect));
1957 BOOL endsOnOrAfterEndOfRange = _selectionRects[i].position >= end - 1;
1958 BOOL nextSelectionRectIsOnNextLine =
1959 !isLastSelectionRect &&
1964 CGRectGetMidY(_selectionRects[i + 1].rect) > CGRectGetMaxY(_selectionRects[i].rect);
1965 if (endsOnOrAfterEndOfRange || isLastSelectionRect || nextSelectionRectIsOnNextLine) {
1966 endSelectionRect = _selectionRects[i].rect;
1971 if (CGRectIsNull(startSelectionRect) || CGRectIsNull(endSelectionRect)) {
1975 CGFloat minX = fmin(CGRectGetMinX(startSelectionRect), CGRectGetMinX(endSelectionRect));
1976 CGFloat maxX = fmax(CGRectGetMaxX(startSelectionRect), CGRectGetMaxX(endSelectionRect));
1977 return CGRectMake(minX, minY, maxX - minX, maxY - minY);
1985 NSArray<UITextSelectionRect*>* rects = [
self
1987 rangeWithNSRange:fml::RangeForCharactersInRange(
1991 (index >= (NSInteger)self.text.length)
1994 if (rects.count == 0) {
2000 CGRect characterAfterCaret = rects[0].rect;
2005 return CGRectMake(characterAfterCaret.origin.x + characterAfterCaret.size.width,
2006 characterAfterCaret.origin.y, 0, characterAfterCaret.size.height);
2008 return CGRectMake(characterAfterCaret.origin.x, characterAfterCaret.origin.y, 0,
2009 characterAfterCaret.size.height);
2011 }
else if (rects.count == 2 && affinity == UITextStorageDirectionForward) {
2014 CGRect characterAfterCaret = rects[1].rect;
2019 return CGRectMake(characterAfterCaret.origin.x + characterAfterCaret.size.width,
2020 characterAfterCaret.origin.y, 0, characterAfterCaret.size.height);
2022 return CGRectMake(characterAfterCaret.origin.x, characterAfterCaret.origin.y, 0,
2023 characterAfterCaret.size.height);
2032 CGRect characterBeforeCaret = rects[0].rect;
2035 return CGRectMake(characterBeforeCaret.origin.x, characterBeforeCaret.origin.y, 0,
2036 characterBeforeCaret.size.height);
2038 return CGRectMake(characterBeforeCaret.origin.x + characterBeforeCaret.size.width,
2039 characterBeforeCaret.origin.y, 0, characterBeforeCaret.size.height);
2043 - (UITextPosition*)closestPositionToPoint:(CGPoint)point {
2044 if ([_selectionRects count] == 0) {
2046 @"Expected a FlutterTextPosition for position (got %@).",
2049 UITextStorageDirection currentAffinity =
2055 rangeWithNSRange:fml::RangeForCharactersInRange(self.text, NSMakeRange(0, self.text.length))];
2056 return [
self closestPositionToPoint:point withinRange:range];
2059 - (NSArray*)selectionRectsForRange:(UITextRange*)range {
2067 @"Expected a FlutterTextPosition for range.start (got %@).", [range.start
class]);
2069 @"Expected a FlutterTextPosition for range.end (got %@).", [range.end
class]);
2072 NSMutableArray* rects = [[NSMutableArray alloc] init];
2073 for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
2074 if (_selectionRects[i].position >= start &&
2075 (_selectionRects[i].position < end ||
2076 (start == end && _selectionRects[i].position <= end))) {
2077 float width = _selectionRects[i].rect.size.width;
2081 CGRect rect = CGRectMake(_selectionRects[i].rect.origin.x, _selectionRects[i].rect.origin.y,
2082 width, _selectionRects[i].rect.size.height);
2085 position:_selectionRects[i].position
2089 self.text, NSMakeRange(0, self.text.length))
2092 [rects addObject:selectionRect];
2098 - (UITextPosition*)closestPositionToPoint:(CGPoint)point withinRange:(UITextRange*)range {
2100 @"Expected a FlutterTextPosition for range.start (got %@).", [range.start
class]);
2102 @"Expected a FlutterTextPosition for range.end (got %@).", [range.end
class]);
2112 NSUInteger _closestRectIndex = 0;
2113 for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
2114 NSUInteger position = _selectionRects[i].position;
2115 if (position >= start && position <= end) {
2118 point, _selectionRects[i].rect, _selectionRects[i].isRTL,
2119 NO, _selectionRects[_closestRectIndex].rect,
2120 _selectionRects[_closestRectIndex].isRTL, verticalPrecision)) {
2122 _closestRectIndex = i;
2129 affinity:UITextStorageDirectionForward];
2135 for (NSUInteger i = MAX(0, _closestRectIndex - 1);
2136 i < MIN(_closestRectIndex + 2, [_selectionRects count]); i++) {
2137 NSUInteger position = _selectionRects[i].position + 1;
2138 if (position >= start && position <= end) {
2140 point, _selectionRects[i].rect, _selectionRects[i].isRTL,
2141 YES, _selectionRects[_closestRectIndex].rect,
2142 _selectionRects[_closestRectIndex].isRTL, verticalPrecision)) {
2145 affinity:UITextStorageDirectionBackward];
2150 return closestPosition;
2153 - (UITextRange*)characterRangeAtPoint:(CGPoint)point {
2156 return [
FlutterTextRange rangeWithNSRange:fml::RangeForCharacterAtIndex(self.text, currentIndex)];
2187 - (void)beginFloatingCursorAtPoint:(CGPoint)point {
2204 [
self.textInputDelegate flutterTextInputView:self
2205 updateFloatingCursor:FlutterFloatingCursorDragStateStart
2206 withClient:_textInputClient
2207 withPosition:@{@"X" : @0, @"Y" : @0}];
2210 - (void)updateFloatingCursorAtPoint:(CGPoint)point {
2211 [
self.textInputDelegate flutterTextInputView:self
2212 updateFloatingCursor:FlutterFloatingCursorDragStateUpdate
2213 withClient:_textInputClient
2215 @"X" : @(point.x - _floatingCursorOffset.x),
2216 @"Y" : @(point.y - _floatingCursorOffset.y)
2220 - (void)endFloatingCursor {
2222 [
self.textInputDelegate flutterTextInputView:self
2223 updateFloatingCursor:FlutterFloatingCursorDragStateEnd
2224 withClient:_textInputClient
2225 withPosition:@{@"X" : @0, @"Y" : @0}];
2228 #pragma mark - UIKeyInput Overrides
2230 - (void)updateEditingState {
2235 NSInteger composingBase = -1;
2236 NSInteger composingExtent = -1;
2241 NSDictionary* state = @{
2242 @"selectionBase" : @(selectionBase),
2243 @"selectionExtent" : @(selectionExtent),
2245 @"selectionIsDirectional" : @(
false),
2246 @"composingBase" : @(composingBase),
2247 @"composingExtent" : @(composingExtent),
2248 @"text" : [NSString stringWithString:self.text],
2251 if (_textInputClient == 0 && _autofillId != nil) {
2252 [
self.textInputDelegate flutterTextInputView:self
2253 updateEditingClient:_textInputClient
2255 withTag:_autofillId];
2257 [
self.textInputDelegate flutterTextInputView:self
2258 updateEditingClient:_textInputClient
2263 - (void)updateEditingStateWithDelta:(
flutter::TextEditingDelta)delta {
2268 NSInteger composingBase = -1;
2269 NSInteger composingExtent = -1;
2275 NSDictionary* deltaToFramework = @{
2276 @"oldText" : @(delta.old_text().c_str()),
2277 @"deltaText" : @(delta.delta_text().c_str()),
2278 @"deltaStart" : @(delta.delta_start()),
2279 @"deltaEnd" : @(delta.delta_end()),
2280 @"selectionBase" : @(selectionBase),
2281 @"selectionExtent" : @(selectionExtent),
2283 @"selectionIsDirectional" : @(
false),
2284 @"composingBase" : @(composingBase),
2285 @"composingExtent" : @(composingExtent),
2288 [_pendingDeltas addObject:deltaToFramework];
2290 if (_pendingDeltas.count == 1) {
2292 dispatch_async(dispatch_get_main_queue(), ^{
2294 if (strongSelf && strongSelf.pendingDeltas.count > 0) {
2295 NSDictionary* deltas = @{
2296 @"deltas" : strongSelf.pendingDeltas,
2299 [strongSelf.textInputDelegate flutterTextInputView:strongSelf
2300 updateEditingClient:strongSelf->_textInputClient
2302 [strongSelf.pendingDeltas removeAllObjects];
2309 return self.text.length > 0;
2312 - (void)insertText:(NSString*)text {
2313 if (
self.temporarilyDeletedComposedCharacter.length > 0 && text.length == 1 && !text.UTF8String &&
2314 [text characterAtIndex:0] == [
self.temporarilyDeletedComposedCharacter characterAtIndex:0]) {
2318 text =
self.temporarilyDeletedComposedCharacter;
2319 self.temporarilyDeletedComposedCharacter = nil;
2322 NSMutableArray<FlutterTextSelectionRect*>* copiedRects =
2323 [[NSMutableArray alloc] initWithCapacity:[_selectionRects count]];
2325 @"Expected a FlutterTextPosition for position (got %@).",
2328 for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
2329 NSUInteger rectPosition = _selectionRects[i].position;
2330 if (rectPosition == insertPosition) {
2331 for (NSUInteger j = 0; j <= text.length; j++) {
2338 if (rectPosition > insertPosition) {
2339 rectPosition = rectPosition + text.length;
2348 _scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
2349 [
self resetScribbleInteractionStatusIfEnding];
2350 self.selectionRects = copiedRects;
2352 [
self replaceRange:_selectedTextRange withText:text];
2355 - (UITextPlaceholder*)insertTextPlaceholderWithSize:(CGSize)size API_AVAILABLE(ios(13.0)) {
2356 [
self.textInputDelegate flutterTextInputView:self
2357 insertTextPlaceholderWithSize:size
2358 withClient:_textInputClient];
2363 - (void)removeTextPlaceholder:(UITextPlaceholder*)textPlaceholder API_AVAILABLE(ios(13.0)) {
2365 [
self.textInputDelegate flutterTextInputView:self removeTextPlaceholder:_textInputClient];
2368 - (void)deleteBackward {
2370 _scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
2371 [
self resetScribbleInteractionStatusIfEnding];
2388 if (oldRange.location > 0) {
2389 NSRange newRange = NSMakeRange(oldRange.location - 1, 1);
2393 NSRange charRange = fml::RangeForCharacterAtIndex(
self.text, oldRange.location - 1);
2394 if (
IsEmoji(
self.text, charRange)) {
2395 newRange = NSMakeRange(charRange.location, oldRange.location - charRange.location);
2407 NSString* deletedText = [
self.text substringWithRange:_selectedTextRange.range];
2408 NSRange deleteFirstCharacterRange = fml::RangeForCharacterAtIndex(deletedText, 0);
2409 self.temporarilyDeletedComposedCharacter =
2410 [deletedText substringWithRange:deleteFirstCharacterRange];
2412 [
self replaceRange:_selectedTextRange withText:@""];
2416 - (void)postAccessibilityNotification:(UIAccessibilityNotifications)notification target:(
id)target {
2417 UIAccessibilityPostNotification(notification, target);
2420 - (void)accessibilityElementDidBecomeFocused {
2421 if ([
self accessibilityElementIsFocused]) {
2425 FML_DCHECK(_backingTextInputAccessibilityObject);
2426 [
self postAccessibilityNotification:UIAccessibilityScreenChangedNotification
2427 target:_backingTextInputAccessibilityObject];
2431 - (BOOL)accessibilityElementsHidden {
2432 return !_accessibilityEnabled;
2441 #pragma mark - Key Events Handling
2442 - (void)pressesBegan:(NSSet<UIPress*>*)presses
2443 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2444 [_textInputPlugin.viewController pressesBegan:presses withEvent:event];
2447 - (void)pressesChanged:(NSSet<UIPress*>*)presses
2448 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2449 [_textInputPlugin.viewController pressesChanged:presses withEvent:event];
2452 - (void)pressesEnded:(NSSet<UIPress*>*)presses
2453 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2454 [_textInputPlugin.viewController pressesEnded:presses withEvent:event];
2457 - (void)pressesCancelled:(NSSet<UIPress*>*)presses
2458 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2459 [_textInputPlugin.viewController pressesCancelled:presses withEvent:event];
2488 - (BOOL)accessibilityElementsHidden {
2495 - (void)enableActiveViewAccessibility;
2512 - (void)enableActiveViewAccessibility {
2513 [
self.target enableActiveViewAccessibility];
2520 @property(nonatomic, readonly)
2521 NSMutableDictionary<NSString*, FlutterTextInputView*>* autofillContext;
2525 @property(nonatomic, readonly) BOOL pendingAutofillRemoval;
2530 @property(nonatomic, readonly) BOOL pendingInputViewRemoval;
2535 @property(nonatomic, strong) UIView* keyboardViewContainer;
2536 @property(nonatomic, strong) UIView* keyboardView;
2537 @property(nonatomic, strong) UIView* cachedFirstResponder;
2538 @property(nonatomic, assign) CGRect keyboardRect;
2539 @property(nonatomic, assign) CGFloat previousPointerYPosition;
2540 @property(nonatomic, assign) CGFloat pointerYVelocity;
2544 NSTimer* _enableFlutterTextInputViewAccessibilityTimer;
2550 self = [
super init];
2553 _textInputDelegate = textInputDelegate;
2554 _autofillContext = [[NSMutableDictionary alloc] init];
2556 _scribbleElements = [[NSMutableDictionary alloc] init];
2557 _keyboardViewContainer = [[UIView alloc] init];
2559 [[NSNotificationCenter defaultCenter] addObserver:self
2560 selector:@selector(handleKeyboardWillShow:)
2561 name:UIKeyboardWillShowNotification
2568 - (void)handleKeyboardWillShow:(NSNotification*)notification {
2569 NSDictionary* keyboardInfo = [notification userInfo];
2570 NSValue* keyboardFrameEnd = [keyboardInfo valueForKey:UIKeyboardFrameEndUserInfoKey];
2571 _keyboardRect = [keyboardFrameEnd CGRectValue];
2578 - (void)removeEnableFlutterTextInputViewAccessibilityTimer {
2579 if (_enableFlutterTextInputViewAccessibilityTimer) {
2580 [_enableFlutterTextInputViewAccessibilityTimer invalidate];
2581 _enableFlutterTextInputViewAccessibilityTimer = nil;
2585 - (UIView<UITextInput>*)textInputView {
2590 [_autofillContext removeAllObjects];
2591 [
self clearTextInputClient];
2592 [
self hideTextInput];
2596 NSString* method = call.
method;
2599 [
self showTextInput];
2601 }
else if ([method isEqualToString:
kHideMethod]) {
2602 [
self hideTextInput];
2605 [
self setTextInputClient:[args[0] intValue] withConfiguration:args[1]];
2609 [
self setPlatformViewTextInputClient];
2612 [
self setTextInputEditingState:args];
2615 [
self clearTextInputClient];
2618 [
self setEditableSizeAndTransform:args];
2621 [
self updateMarkedRect:args];
2624 [
self triggerAutofillSave:[args boolValue]];
2630 [
self setSelectionRects:args];
2633 [
self setSelectionRects:args];
2636 [
self startLiveTextInput];
2639 [
self updateConfig:args];
2642 CGFloat pointerY = (CGFloat)[args[
@"pointerY"] doubleValue];
2643 [
self handlePointerMove:pointerY];
2646 CGFloat pointerY = (CGFloat)[args[
@"pointerY"] doubleValue];
2647 [
self handlePointerUp:pointerY];
2654 - (void)handlePointerUp:(CGFloat)pointerY {
2655 if (_keyboardView.superview != nil) {
2658 UIScreen* screen = _viewController.flutterScreenIfViewLoaded;
2659 CGFloat screenHeight = screen.bounds.size.height;
2660 CGFloat keyboardHeight = _keyboardRect.size.height;
2662 BOOL shouldDismissKeyboardBasedOnVelocity = _pointerYVelocity < 0;
2663 [UIView animateWithDuration:kKeyboardAnimationTimeToCompleteion
2665 double keyboardDestination =
2666 shouldDismissKeyboardBasedOnVelocity ? screenHeight : screenHeight - keyboardHeight;
2667 _keyboardViewContainer.frame = CGRectMake(
2668 0, keyboardDestination, _viewController.flutterScreenIfViewLoaded.bounds.size.width,
2669 _keyboardViewContainer.frame.size.height);
2671 completion:^(BOOL finished) {
2672 if (shouldDismissKeyboardBasedOnVelocity) {
2673 [
self.textInputDelegate flutterTextInputView:self.activeView
2674 didResignFirstResponderWithTextInputClient:self.activeView.textInputClient];
2675 [
self dismissKeyboardScreenshot];
2677 [
self showKeyboardAndRemoveScreenshot];
2683 - (void)dismissKeyboardScreenshot {
2684 for (UIView* subView in _keyboardViewContainer.subviews) {
2685 [subView removeFromSuperview];
2689 - (void)showKeyboardAndRemoveScreenshot {
2690 [UIView setAnimationsEnabled:NO];
2691 [_cachedFirstResponder becomeFirstResponder];
2695 dispatch_get_main_queue(), ^{
2696 [UIView setAnimationsEnabled:YES];
2697 [
self dismissKeyboardScreenshot];
2701 - (void)handlePointerMove:(CGFloat)pointerY {
2703 UIScreen* screen = _viewController.flutterScreenIfViewLoaded;
2704 CGFloat screenHeight = screen.bounds.size.height;
2705 CGFloat keyboardHeight = _keyboardRect.size.height;
2706 if (screenHeight - keyboardHeight <= pointerY) {
2708 if (_keyboardView.superview == nil) {
2710 [
self takeKeyboardScreenshotAndDisplay];
2711 [
self hideKeyboardWithoutAnimationAndAvoidCursorDismissUpdate];
2713 [
self setKeyboardContainerHeight:pointerY];
2714 _pointerYVelocity = _previousPointerYPosition - pointerY;
2717 if (_keyboardView.superview != nil) {
2719 _keyboardViewContainer.frame = _keyboardRect;
2720 _pointerYVelocity = _previousPointerYPosition - pointerY;
2723 _previousPointerYPosition = pointerY;
2726 - (void)setKeyboardContainerHeight:(CGFloat)pointerY {
2727 CGRect frameRect = _keyboardRect;
2728 frameRect.origin.y = pointerY;
2729 _keyboardViewContainer.frame = frameRect;
2732 - (void)hideKeyboardWithoutAnimationAndAvoidCursorDismissUpdate {
2733 [UIView setAnimationsEnabled:NO];
2735 _cachedFirstResponder =
2737 ? flutterApplication.keyWindow.flutterFirstResponder
2738 :
self.viewController.flutterWindowSceneIfViewLoaded.keyWindow.flutterFirstResponder;
2740 _activeView.preventCursorDismissWhenResignFirstResponder = YES;
2741 [_cachedFirstResponder resignFirstResponder];
2742 _activeView.preventCursorDismissWhenResignFirstResponder = NO;
2743 [UIView setAnimationsEnabled:YES];
2746 - (void)takeKeyboardScreenshotAndDisplay {
2748 UIScreen* screen = _viewController.flutterScreenIfViewLoaded;
2749 UIView* keyboardSnap = [screen snapshotViewAfterScreenUpdates:YES];
2750 keyboardSnap = [keyboardSnap resizableSnapshotViewFromRect:_keyboardRect
2751 afterScreenUpdates:YES
2752 withCapInsets:UIEdgeInsetsZero];
2753 _keyboardView = keyboardSnap;
2754 [_keyboardViewContainer addSubview:_keyboardView];
2755 if (_keyboardViewContainer.superview == nil) {
2757 UIView* rootView = flutterApplication
2758 ? flutterApplication.delegate.window.rootViewController.view
2759 :
self.viewController.viewIfLoaded.window.rootViewController.view;
2760 [rootView addSubview:_keyboardViewContainer];
2762 _keyboardViewContainer.layer.zPosition = NSIntegerMax;
2763 _keyboardViewContainer.frame = _keyboardRect;
2766 - (BOOL)showEditMenu:(NSDictionary*)args API_AVAILABLE(ios(16.0)) {
2767 if (!
self.activeView.isFirstResponder) {
2770 NSDictionary<NSString*, NSNumber*>* encodedTargetRect = args[@"targetRect"];
2771 CGRect globalTargetRect = CGRectMake(
2772 [encodedTargetRect[
@"x"] doubleValue], [encodedTargetRect[
@"y"] doubleValue],
2773 [encodedTargetRect[
@"width"] doubleValue], [encodedTargetRect[
@"height"] doubleValue]);
2774 CGRect localTargetRect = [
self.hostView convertRect:globalTargetRect toView:self.activeView];
2775 [
self.activeView showEditMenuWithTargetRect:localTargetRect items:args[@"items"]];
2779 - (void)hideEditMenu {
2780 [
self.activeView hideEditMenu];
2783 - (void)setEditableSizeAndTransform:(NSDictionary*)dictionary {
2784 NSArray* transform = dictionary[@"transform"];
2785 [_activeView setEditableTransform:transform];
2786 const int leftIndex = 12;
2787 const int topIndex = 13;
2791 CGRectMake([transform[leftIndex] intValue], [transform[topIndex] intValue],
2792 [dictionary[
@"width"] intValue], [dictionary[
@"height"] intValue]);
2794 CGRectMake(0, 0, [dictionary[
@"width"] intValue], [dictionary[
@"height"] intValue]);
2795 _activeView.tintColor = [UIColor clearColor];
2800 if (@available(iOS 17, *)) {
2808 CGRectMake([transform[leftIndex] intValue], [transform[topIndex] intValue], 0, 0);
2813 - (void)updateMarkedRect:(NSDictionary*)dictionary {
2814 NSAssert(dictionary[
@"x"] != nil && dictionary[
@"y"] != nil && dictionary[
@"width"] != nil &&
2815 dictionary[
@"height"] != nil,
2816 @"Expected a dictionary representing a CGRect, got %@", dictionary);
2817 CGRect rect = CGRectMake([dictionary[
@"x"] doubleValue], [dictionary[
@"y"] doubleValue],
2818 [dictionary[
@"width"] doubleValue], [dictionary[
@"height"] doubleValue]);
2819 _activeView.markedRect = rect.size.width < 0 && rect.size.height < 0 ?
kInvalidFirstRect : rect;
2822 - (void)setSelectionRects:(NSArray*)encodedRects {
2823 NSMutableArray<FlutterTextSelectionRect*>* rectsAsRect =
2824 [[NSMutableArray alloc] initWithCapacity:[encodedRects count]];
2825 for (NSUInteger i = 0; i < [encodedRects count]; i++) {
2826 NSArray<NSNumber*>* encodedRect = encodedRects[i];
2828 selectionRectWithRect:CGRectMake([encodedRect[0] floatValue],
2829 [encodedRect[1] floatValue],
2830 [encodedRect[2] floatValue],
2831 [encodedRect[3] floatValue])
2832 position:[encodedRect[4] unsignedIntegerValue]
2833 writingDirection:[encodedRect[5] unsignedIntegerValue] == 1
2834 ? NSWritingDirectionLeftToRight
2835 : NSWritingDirectionRightToLeft]];
2841 _activeView.selectionRects = rectsAsRect;
2844 - (void)startLiveTextInput {
2845 if (@available(iOS 15.0, *)) {
2846 if (_activeView == nil || !_activeView.isFirstResponder) {
2849 [_activeView captureTextFromCamera:nil];
2853 - (void)showTextInput {
2854 _activeView.viewResponder = _viewResponder;
2855 [
self addToInputParentViewIfNeeded:_activeView];
2856 [_activeView becomeFirstResponder];
2859 - (void)enableActiveViewAccessibility {
2860 if (_activeView.isFirstResponder) {
2861 _activeView.accessibilityEnabled = YES;
2863 [
self removeEnableFlutterTextInputViewAccessibilityTimer];
2866 - (void)hideTextInput {
2867 [_activeView resignFirstResponder];
2874 [_activeView removeFromSuperview];
2875 [_inputHider removeFromSuperview];
2880 - (void)triggerAutofillSave:(BOOL)saveEntries {
2881 [_activeView resignFirstResponder];
2886 [
self cleanUpViewHierarchy:YES clearText:YES delayRemoval:NO];
2887 [_autofillContext removeAllObjects];
2888 [
self changeInputViewsAutofillVisibility:YES];
2890 [_autofillContext removeAllObjects];
2893 [
self cleanUpViewHierarchy:YES clearText:!saveEntries delayRemoval:NO];
2897 [_activeView removeFromSuperview];
2898 [_inputHider removeFromSuperview];
2903 [
self addToInputParentViewIfNeeded:_activeView];
2906 - (void)setPlatformViewTextInputClient {
2910 [
self removeEnableFlutterTextInputViewAccessibilityTimer];
2911 _activeView.accessibilityEnabled = NO;
2912 [_activeView removeFromSuperview];
2913 [_inputHider removeFromSuperview];
2916 - (void)setTextInputClient:(
int)client withConfiguration:(NSDictionary*)configuration {
2917 [
self resetAllClientIds];
2925 [
self changeInputViewsAutofillVisibility:NO];
2929 case kFlutterAutofillTypeNone:
2930 self.activeView = [
self createInputViewWith:configuration];
2932 case kFlutterAutofillTypeRegular:
2935 self.activeView = [
self updateAndShowAutofillViews:nil
2936 focusedField:configuration
2937 isPasswordRelated:NO];
2939 case kFlutterAutofillTypePassword:
2940 self.activeView = [
self updateAndShowAutofillViews:configuration[kAssociatedAutofillFields]
2941 focusedField:configuration
2942 isPasswordRelated:YES];
2945 [_activeView setTextInputClient:client];
2946 [_activeView reloadInputViews];
2958 [
self cleanUpViewHierarchy:NO clearText:YES delayRemoval:YES];
2968 if (!_enableFlutterTextInputViewAccessibilityTimer) {
2969 _enableFlutterTextInputViewAccessibilityTimer =
2970 [NSTimer scheduledTimerWithTimeInterval:kUITextInputAccessibilityEnablingDelaySeconds
2972 selector:@selector(enableActiveViewAccessibility)
2986 [_autofillContext removeObjectForKey:autofillId];
2989 [newView configureWithDictionary:configuration];
2990 [
self addToInputParentViewIfNeeded:newView];
2994 if (autofillId &&
AutofillTypeOf(field) == kFlutterAutofillTypeNone) {
2995 [_autofillContext removeObjectForKey:autofillId];
3002 focusedField:(NSDictionary*)focusedField
3003 isPasswordRelated:(BOOL)isPassword {
3006 NSAssert(focusedId,
@"autofillId must not be null for the focused field: %@", focusedField);
3011 focused = [
self getOrCreateAutofillableView:focusedField isPasswordAutofill:isPassword];
3012 [_autofillContext removeObjectForKey:focusedId];
3015 for (NSDictionary* field in fields) {
3017 NSAssert(autofillId,
@"autofillId must not be null for field: %@", field);
3019 BOOL hasHints =
AutofillTypeOf(field) != kFlutterAutofillTypeNone;
3020 BOOL isFocused = [focusedId isEqualToString:autofillId];
3023 focused = [
self getOrCreateAutofillableView:field isPasswordAutofill:isPassword];
3028 _autofillContext[autofillId] = isFocused ? focused
3029 : [
self getOrCreateAutofillableView:field
3030 isPasswordAutofill:isPassword];
3033 [_autofillContext removeObjectForKey:autofillId];
3037 NSAssert(focused,
@"The current focused input view must not be nil.");
3047 isPasswordAutofill:(BOOL)needsPasswordAutofill {
3053 inputView = [inputView initWithOwner:self];
3054 [
self addToInputParentViewIfNeeded:inputView];
3057 [inputView configureWithDictionary:field];
3062 - (UIView*)hostView {
3063 UIView* host = _viewController.view;
3064 NSAssert(host !=
nullptr,
3065 @"The application must have a host view since the keyboard client "
3066 @"must be part of the responder chain to function. The host view controller is %@",
3072 - (NSArray<UIView*>*)textInputViews {
3073 return _inputHider.subviews;
3086 - (void)cleanUpViewHierarchy:(BOOL)includeActiveView
3087 clearText:(BOOL)clearText
3088 delayRemoval:(BOOL)delayRemoval {
3089 for (UIView* view in
self.textInputViews) {
3091 (includeActiveView || view != _activeView)) {
3093 if (_autofillContext[inputView.autofillId] != view) {
3095 [inputView replaceRangeLocal:NSMakeRange(0, inputView.text.length) withText:@""];
3098 [inputView performSelector:@selector(removeFromSuperview) withObject:nil afterDelay:0.1];
3100 [inputView removeFromSuperview];
3109 - (void)changeInputViewsAutofillVisibility:(BOOL)newVisibility {
3110 for (UIView* view in
self.textInputViews) {
3113 inputView.isVisibleToAutofill = newVisibility;
3125 - (void)resetAllClientIds {
3126 for (UIView* view in
self.textInputViews) {
3129 [inputView setTextInputClient:0];
3135 if (![inputView isDescendantOfView:_inputHider]) {
3136 [_inputHider addSubview:inputView];
3139 if (_viewController.view == nil) {
3145 UIView* parentView =
self.hostView;
3146 if (_inputHider.superview != parentView) {
3147 [parentView addSubview:_inputHider];
3151 - (void)setTextInputEditingState:(NSDictionary*)state {
3152 [_activeView setTextInputState:state];
3155 - (void)clearTextInputClient {
3156 [_activeView setTextInputClient:0];
3157 _activeView.frame = CGRectZero;
3159 [
self removeEnableFlutterTextInputViewAccessibilityTimer];
3160 _activeView.accessibilityEnabled = NO;
3167 if (_autofillContext.count == 0) {
3169 if (_activeView.isFirstResponder) {
3176 [_activeView removeFromSuperview];
3177 [_inputHider removeFromSuperview];
3188 - (void)updateConfig:(NSDictionary*)dictionary {
3189 BOOL isSecureTextEntry = [dictionary[kSecureTextEntry] boolValue];
3190 for (UIView* view in
self.textInputViews) {
3197 if (inputView.isSecureTextEntry != isSecureTextEntry) {
3198 inputView.secureTextEntry = isSecureTextEntry;
3199 [inputView reloadInputViews];
3205 #pragma mark UIIndirectScribbleInteractionDelegate
3207 - (BOOL)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
3208 isElementFocused:(UIScribbleElementIdentifier)elementIdentifier
3209 API_AVAILABLE(ios(14.0)) {
3210 return _activeView.scribbleFocusStatus == FlutterScribbleFocusStatusFocused;
3213 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
3214 focusElementIfNeeded:(UIScribbleElementIdentifier)elementIdentifier
3215 referencePoint:(CGPoint)focusReferencePoint
3216 completion:(
void (^)(UIResponder<UITextInput>* focusedInput))completion
3217 API_AVAILABLE(ios(14.0)) {
3218 _activeView.scribbleFocusStatus = FlutterScribbleFocusStatusFocusing;
3219 [_indirectScribbleDelegate flutterTextInputPlugin:self
3220 focusElement:elementIdentifier
3221 atPoint:focusReferencePoint
3222 result:^(id _Nullable result) {
3223 _activeView.scribbleFocusStatus =
3224 FlutterScribbleFocusStatusFocused;
3225 completion(_activeView);
3229 - (BOOL)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
3230 shouldDelayFocusForElement:(UIScribbleElementIdentifier)elementIdentifier
3231 API_AVAILABLE(ios(14.0)) {
3235 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
3236 willBeginWritingInElement:(UIScribbleElementIdentifier)elementIdentifier
3237 API_AVAILABLE(ios(14.0)) {
3240 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
3241 didFinishWritingInElement:(UIScribbleElementIdentifier)elementIdentifier
3242 API_AVAILABLE(ios(14.0)) {
3245 - (CGRect)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
3246 frameForElement:(UIScribbleElementIdentifier)elementIdentifier
3247 API_AVAILABLE(ios(14.0)) {
3248 NSValue* elementValue = [_scribbleElements objectForKey:elementIdentifier];
3249 if (elementValue == nil) {
3252 return [elementValue CGRectValue];
3255 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
3256 requestElementsInRect:(CGRect)rect
3258 (
void (^)(NSArray<UIScribbleElementIdentifier>* elements))completion
3259 API_AVAILABLE(ios(14.0)) {
3260 [_indirectScribbleDelegate
3261 flutterTextInputPlugin:self
3262 requestElementsInRect:rect
3263 result:^(id _Nullable result) {
3264 NSMutableArray<UIScribbleElementIdentifier>* elements =
3265 [[NSMutableArray alloc] init];
3266 if ([result isKindOfClass:[NSArray class]]) {
3267 for (NSArray* elementArray in result) {
3268 [elements addObject:elementArray[0]];
3271 valueWithCGRect:CGRectMake(
3272 [elementArray[1] floatValue],
3273 [elementArray[2] floatValue],
3274 [elementArray[3] floatValue],
3275 [elementArray[4] floatValue])]
3276 forKey:elementArray[0]];
3279 completion(elements);
3283 #pragma mark - Methods related to Scribble support
3287 if (@available(iOS 14.0, *)) {
3289 if (parentView != nil) {
3290 UIIndirectScribbleInteraction* scribbleInteraction = [[UIIndirectScribbleInteraction alloc]
3291 initWithDelegate:(id<UIIndirectScribbleInteractionDelegate>)self];
3292 [parentView addInteraction:scribbleInteraction];
3299 - (void)resetViewResponder {
3300 _viewResponder = nil;
3304 #pragma mark FlutterKeySecondaryResponder
3310 - (BOOL)handlePress:(nonnull FlutterUIPressProxy*)press API_AVAILABLE(ios(13.4)) {
3319 - (id)flutterFirstResponder {
3320 if (
self.isFirstResponder) {
3323 for (UIView* subView in
self.subviews) {
3324 UIView* firstResponder = subView.flutterFirstResponder;
3325 if (firstResponder) {
3326 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 _pendingAutofillRemoval
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 kEnableInlinePrediction
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
BOOL _pendingInputViewRemoval
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