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"
37 #pragma mark - TextInput channel method names.
46 @"TextInput.setEditableSizeAndTransform";
57 @"TextInput.onPointerMoveForInteractiveKeyboard";
59 @"TextInput.onPointerUpForInteractiveKeyboard";
61 #pragma mark - TextInputConfiguration Field Names
82 #pragma mark - Static Functions
85 static BOOL
IsEmoji(NSString* text, NSRange charRange) {
87 BOOL gotCodePoint = [text getBytes:&codePoint
88 maxLength:
sizeof(codePoint)
90 encoding:NSUTF32StringEncoding
94 return gotCodePoint && u_hasBinaryProperty(codePoint, UCHAR_EMOJI);
102 NSString* inputType = type[
@"name"];
103 return ![inputType isEqualToString:
@"TextInputType.none"];
106 NSString* inputType = type[
@"name"];
107 if ([inputType isEqualToString:
@"TextInputType.address"]) {
108 return UIKeyboardTypeDefault;
110 if ([inputType isEqualToString:
@"TextInputType.datetime"]) {
111 return UIKeyboardTypeNumbersAndPunctuation;
113 if ([inputType isEqualToString:
@"TextInputType.emailAddress"]) {
114 return UIKeyboardTypeEmailAddress;
116 if ([inputType isEqualToString:
@"TextInputType.multiline"]) {
117 return UIKeyboardTypeDefault;
119 if ([inputType isEqualToString:
@"TextInputType.name"]) {
120 return UIKeyboardTypeNamePhonePad;
122 if ([inputType isEqualToString:
@"TextInputType.number"]) {
123 if ([type[
@"signed"] boolValue]) {
124 return UIKeyboardTypeNumbersAndPunctuation;
126 if ([type[
@"decimal"] boolValue]) {
127 return UIKeyboardTypeDecimalPad;
129 return UIKeyboardTypeNumberPad;
131 if ([inputType isEqualToString:
@"TextInputType.phone"]) {
132 return UIKeyboardTypePhonePad;
134 if ([inputType isEqualToString:
@"TextInputType.text"]) {
135 return UIKeyboardTypeDefault;
137 if ([inputType isEqualToString:
@"TextInputType.url"]) {
138 return UIKeyboardTypeURL;
140 if ([inputType isEqualToString:
@"TextInputType.visiblePassword"]) {
141 return UIKeyboardTypeASCIICapable;
143 return UIKeyboardTypeDefault;
147 NSString* textCapitalization = type[
@"textCapitalization"];
148 if ([textCapitalization isEqualToString:
@"TextCapitalization.characters"]) {
149 return UITextAutocapitalizationTypeAllCharacters;
150 }
else if ([textCapitalization isEqualToString:
@"TextCapitalization.sentences"]) {
151 return UITextAutocapitalizationTypeSentences;
152 }
else if ([textCapitalization isEqualToString:
@"TextCapitalization.words"]) {
153 return UITextAutocapitalizationTypeWords;
155 return UITextAutocapitalizationTypeNone;
163 if ([inputType isEqualToString:
@"TextInputAction.unspecified"]) {
164 return UIReturnKeyDefault;
167 if ([inputType isEqualToString:
@"TextInputAction.done"]) {
168 return UIReturnKeyDone;
171 if ([inputType isEqualToString:
@"TextInputAction.go"]) {
172 return UIReturnKeyGo;
175 if ([inputType isEqualToString:
@"TextInputAction.send"]) {
176 return UIReturnKeySend;
179 if ([inputType isEqualToString:
@"TextInputAction.search"]) {
180 return UIReturnKeySearch;
183 if ([inputType isEqualToString:
@"TextInputAction.next"]) {
184 return UIReturnKeyNext;
187 if ([inputType isEqualToString:
@"TextInputAction.continueAction"]) {
188 return UIReturnKeyContinue;
191 if ([inputType isEqualToString:
@"TextInputAction.join"]) {
192 return UIReturnKeyJoin;
195 if ([inputType isEqualToString:
@"TextInputAction.route"]) {
196 return UIReturnKeyRoute;
199 if ([inputType isEqualToString:
@"TextInputAction.emergencyCall"]) {
200 return UIReturnKeyEmergencyCall;
203 if ([inputType isEqualToString:
@"TextInputAction.newline"]) {
204 return UIReturnKeyDefault;
208 return UIReturnKeyDefault;
212 if (!hints || hints.count == 0) {
217 NSString* hint = hints[0];
218 if ([hint isEqualToString:
@"addressCityAndState"]) {
219 return UITextContentTypeAddressCityAndState;
222 if ([hint isEqualToString:
@"addressState"]) {
223 return UITextContentTypeAddressState;
226 if ([hint isEqualToString:
@"addressCity"]) {
227 return UITextContentTypeAddressCity;
230 if ([hint isEqualToString:
@"sublocality"]) {
231 return UITextContentTypeSublocality;
234 if ([hint isEqualToString:
@"streetAddressLine1"]) {
235 return UITextContentTypeStreetAddressLine1;
238 if ([hint isEqualToString:
@"streetAddressLine2"]) {
239 return UITextContentTypeStreetAddressLine2;
242 if ([hint isEqualToString:
@"countryName"]) {
243 return UITextContentTypeCountryName;
246 if ([hint isEqualToString:
@"fullStreetAddress"]) {
247 return UITextContentTypeFullStreetAddress;
250 if ([hint isEqualToString:
@"postalCode"]) {
251 return UITextContentTypePostalCode;
254 if ([hint isEqualToString:
@"location"]) {
255 return UITextContentTypeLocation;
258 if ([hint isEqualToString:
@"creditCardNumber"]) {
259 return UITextContentTypeCreditCardNumber;
262 if ([hint isEqualToString:
@"email"]) {
263 return UITextContentTypeEmailAddress;
266 if ([hint isEqualToString:
@"jobTitle"]) {
267 return UITextContentTypeJobTitle;
270 if ([hint isEqualToString:
@"givenName"]) {
271 return UITextContentTypeGivenName;
274 if ([hint isEqualToString:
@"middleName"]) {
275 return UITextContentTypeMiddleName;
278 if ([hint isEqualToString:
@"familyName"]) {
279 return UITextContentTypeFamilyName;
282 if ([hint isEqualToString:
@"name"]) {
283 return UITextContentTypeName;
286 if ([hint isEqualToString:
@"namePrefix"]) {
287 return UITextContentTypeNamePrefix;
290 if ([hint isEqualToString:
@"nameSuffix"]) {
291 return UITextContentTypeNameSuffix;
294 if ([hint isEqualToString:
@"nickname"]) {
295 return UITextContentTypeNickname;
298 if ([hint isEqualToString:
@"organizationName"]) {
299 return UITextContentTypeOrganizationName;
302 if ([hint isEqualToString:
@"telephoneNumber"]) {
303 return UITextContentTypeTelephoneNumber;
306 if ([hint isEqualToString:
@"password"]) {
307 return UITextContentTypePassword;
310 if ([hint isEqualToString:
@"oneTimeCode"]) {
311 return UITextContentTypeOneTimeCode;
314 if ([hint isEqualToString:
@"newPassword"]) {
315 return UITextContentTypeNewPassword;
383 typedef NS_ENUM(NSInteger, FlutterAutofillType) {
387 kFlutterAutofillTypeNone,
388 kFlutterAutofillTypeRegular,
389 kFlutterAutofillTypePassword,
399 if (isSecureTextEntry) {
406 if ([contentType isEqualToString:UITextContentTypePassword] ||
407 [contentType isEqualToString:UITextContentTypeUsername]) {
411 if ([contentType isEqualToString:UITextContentTypeNewPassword]) {
421 return kFlutterAutofillTypePassword;
426 return kFlutterAutofillTypePassword;
431 return !autofill || [contentType isEqualToString:
@""] ? kFlutterAutofillTypeNone
432 : kFlutterAutofillTypeRegular;
436 return fabsf(x - y) <= delta;
462 CGRect selectionRect,
463 BOOL selectionRectIsRTL,
464 BOOL useTrailingBoundaryOfSelectionRect,
465 CGRect otherSelectionRect,
466 BOOL otherSelectionRectIsRTL,
467 CGFloat verticalPrecision) {
469 if (CGRectContainsPoint(
471 selectionRect.origin.x + ((useTrailingBoundaryOfSelectionRect ^ selectionRectIsRTL)
472 ? 0.5 * selectionRect.size.width
474 selectionRect.origin.y, 0.5 * selectionRect.size.width, selectionRect.size.height),
479 CGPoint pointForSelectionRect = CGPointMake(
480 selectionRect.origin.x +
481 (selectionRectIsRTL ^ useTrailingBoundaryOfSelectionRect ? selectionRect.size.width : 0),
482 selectionRect.origin.y + selectionRect.size.height * 0.5);
483 float yDist = fabs(pointForSelectionRect.y - point.y);
484 float xDist = fabs(pointForSelectionRect.x - point.x);
487 CGPoint pointForOtherSelectionRect = CGPointMake(
488 otherSelectionRect.origin.x + (otherSelectionRectIsRTL ? otherSelectionRect.size.width : 0),
489 otherSelectionRect.origin.y + otherSelectionRect.size.height * 0.5);
490 float yDistOther = fabs(pointForOtherSelectionRect.y - point.y);
491 float xDistOther = fabs(pointForOtherSelectionRect.x - point.x);
496 BOOL isCloserVertically = yDist < yDistOther - verticalPrecision;
498 BOOL isAboveBottomOfLine = point.y <= selectionRect.origin.y + selectionRect.size.height;
499 BOOL isCloserHorizontally = xDist < xDistOther;
500 BOOL isBelowBottomOfLine = point.y > selectionRect.origin.y + selectionRect.size.height;
503 if (selectionRectIsRTL) {
504 isFarther = selectionRect.origin.x < otherSelectionRect.origin.x;
506 isFarther = selectionRect.origin.x +
507 (useTrailingBoundaryOfSelectionRect ? selectionRect.size.width : 0) >
508 otherSelectionRect.origin.x;
510 return (isCloserVertically ||
511 (isEqualVertically &&
512 ((isAboveBottomOfLine && isCloserHorizontally) || (isBelowBottomOfLine && isFarther))));
515 #pragma mark - FlutterTextPosition
519 + (instancetype)positionWithIndex:(NSUInteger)index {
520 return [[
FlutterTextPosition alloc] initWithIndex:index affinity:UITextStorageDirectionForward];
523 + (instancetype)positionWithIndex:(NSUInteger)index affinity:(UITextStorageDirection)affinity {
527 - (instancetype)initWithIndex:(NSUInteger)index affinity:(UITextStorageDirection)affinity {
538 #pragma mark - FlutterTextRange
542 + (instancetype)rangeWithNSRange:(NSRange)range {
546 - (instancetype)initWithNSRange:(NSRange)range {
554 - (UITextPosition*)start {
556 affinity:UITextStorageDirectionForward];
559 - (UITextPosition*)end {
561 affinity:UITextStorageDirectionBackward];
565 return self.range.length == 0;
568 - (id)copyWithZone:(NSZone*)zone {
573 return NSEqualRanges(
self.
range, other.
range);
577 #pragma mark - FlutterTokenizer
587 - (instancetype)initWithTextInput:(UIResponder<UITextInput>*)textInput {
589 @"The FlutterTokenizer can only be used in a FlutterTextInputView");
590 self = [
super initWithTextInput:textInput];
597 - (UITextRange*)rangeEnclosingPosition:(UITextPosition*)position
598 withGranularity:(UITextGranularity)granularity
599 inDirection:(UITextDirection)direction {
601 switch (granularity) {
602 case UITextGranularityLine:
605 result = [
self lineEnclosingPosition:position inDirection:direction];
607 case UITextGranularityCharacter:
608 case UITextGranularityWord:
609 case UITextGranularitySentence:
610 case UITextGranularityParagraph:
611 case UITextGranularityDocument:
613 result = [
super rangeEnclosingPosition:position
614 withGranularity:granularity
615 inDirection:direction];
621 - (UITextRange*)lineEnclosingPosition:(UITextPosition*)position
622 inDirection:(UITextDirection)direction {
624 if (@available(iOS 17.0, *)) {
629 if (flutterPosition.
index > _textInputView.text.length ||
630 (flutterPosition.
index == _textInputView.text.length &&
631 direction == UITextStorageDirectionForward)) {
637 NSString* textAfter = [_textInputView
638 textInRange:[_textInputView textRangeFromPosition:position
639 toPosition:[_textInputView endOfDocument]]];
640 NSArray<NSString*>* linesAfter = [textAfter componentsSeparatedByString:@"\n"];
641 NSInteger offSetToLineBreak = [linesAfter firstObject].length;
642 UITextPosition* lineBreakAfter = [_textInputView positionFromPosition:position
643 offset:offSetToLineBreak];
645 NSString* textBefore = [_textInputView
646 textInRange:[_textInputView textRangeFromPosition:[_textInputView beginningOfDocument]
647 toPosition:position]];
648 NSArray<NSString*>* linesBefore = [textBefore componentsSeparatedByString:@"\n"];
649 NSInteger offSetFromLineBreak = [linesBefore lastObject].length;
650 UITextPosition* lineBreakBefore = [_textInputView positionFromPosition:position
651 offset:-offSetFromLineBreak];
653 return [_textInputView textRangeFromPosition:lineBreakBefore toPosition:lineBreakAfter];
658 #pragma mark - FlutterTextSelectionRect
662 @synthesize rect = _rect;
668 + (instancetype)selectionRectWithRectAndInfo:(CGRect)rect
669 position:(NSUInteger)position
670 writingDirection:(NSWritingDirection)writingDirection
671 containsStart:(BOOL)containsStart
672 containsEnd:(BOOL)containsEnd
673 isVertical:(BOOL)isVertical {
676 writingDirection:writingDirection
677 containsStart:containsStart
678 containsEnd:containsEnd
679 isVertical:isVertical];
682 + (instancetype)selectionRectWithRect:(CGRect)rect position:(NSUInteger)position {
685 writingDirection:NSWritingDirectionNatural
691 + (instancetype)selectionRectWithRect:(CGRect)rect
692 position:(NSUInteger)position
693 writingDirection:(NSWritingDirection)writingDirection {
696 writingDirection:writingDirection
702 - (instancetype)initWithRectAndInfo:(CGRect)rect
703 position:(NSUInteger)position
704 writingDirection:(NSWritingDirection)writingDirection
705 containsStart:(BOOL)containsStart
706 containsEnd:(BOOL)containsEnd
707 isVertical:(BOOL)isVertical {
721 return _writingDirection == NSWritingDirectionRightToLeft;
726 #pragma mark - FlutterTextPlaceholder
730 - (NSArray<UITextSelectionRect*>*)rects {
746 @property(nonatomic, retain, readonly) UITextField*
textField;
750 UITextField* _textField;
755 _textField = [[UITextField alloc] init];
760 - (BOOL)isKindOfClass:(Class)aClass {
761 return [
super isKindOfClass:aClass] || (aClass == [UITextField class]);
764 - (NSMethodSignature*)methodSignatureForSelector:(
SEL)aSelector {
765 NSMethodSignature* signature = [
super methodSignatureForSelector:aSelector];
767 signature = [
self.textField methodSignatureForSelector:aSelector];
772 - (void)forwardInvocation:(NSInvocation*)anInvocation {
773 [anInvocation invokeWithTarget:self.textField];
779 @property(nonatomic, readonly, weak) id<FlutterTextInputDelegate> textInputDelegate;
780 @property(nonatomic, readonly) UIView* hostView;
785 @property(nonatomic, copy) NSString* autofillId;
786 @property(nonatomic, readonly) CATransform3D editableTransform;
787 @property(nonatomic, assign) CGRect markedRect;
789 @property(nonatomic, assign) BOOL preventCursorDismissWhenResignFirstResponder;
790 @property(nonatomic) BOOL isVisibleToAutofill;
791 @property(nonatomic, assign) BOOL accessibilityEnabled;
792 @property(nonatomic, assign)
int textInputClient;
796 @property(nonatomic, copy) NSString* temporarilyDeletedComposedCharacter;
797 @property(nonatomic, assign) CGRect editMenuTargetRect;
799 - (void)setEditableTransform:(NSArray*)matrix;
803 int _textInputClient;
817 UITextInteraction* _textInteraction
API_AVAILABLE(ios(13.0));
820 @synthesize tokenizer = _tokenizer;
823 self = [
super initWithFrame:CGRectZero];
826 _textInputClient = 0;
828 _preventCursorDismissWhenResignFirstResponder = NO;
831 _text = [[NSMutableString alloc] init];
836 _pendingDeltas = [[NSMutableArray alloc] init];
839 _editableTransform = CATransform3D();
842 _autocapitalizationType = UITextAutocapitalizationTypeSentences;
843 _autocorrectionType = UITextAutocorrectionTypeDefault;
844 _spellCheckingType = UITextSpellCheckingTypeDefault;
845 _enablesReturnKeyAutomatically = NO;
846 _keyboardAppearance = UIKeyboardAppearanceDefault;
847 _keyboardType = UIKeyboardTypeDefault;
848 _returnKeyType = UIReturnKeyDone;
849 _secureTextEntry = NO;
850 _enableDeltaModel = NO;
852 _accessibilityEnabled = NO;
853 _smartQuotesType = UITextSmartQuotesTypeYes;
854 _smartDashesType = UITextSmartDashesTypeYes;
855 _selectionRects = [[NSArray alloc] init];
857 if (@available(iOS 14.0, *)) {
858 UIScribbleInteraction* interaction = [[UIScribbleInteraction alloc] initWithDelegate:self];
859 [
self addInteraction:interaction];
863 if (@available(iOS 16.0, *)) {
864 _editMenuInteraction = [[UIEditMenuInteraction alloc] initWithDelegate:self];
865 [
self addInteraction:_editMenuInteraction];
871 - (UIMenu*)editMenuInteraction:(UIEditMenuInteraction*)interaction
872 menuForConfiguration:(UIEditMenuConfiguration*)configuration
873 suggestedActions:(NSArray<UIMenuElement*>*)suggestedActions API_AVAILABLE(ios(16.0)) {
874 return [UIMenu menuWithChildren:suggestedActions];
877 - (void)editMenuInteraction:(UIEditMenuInteraction*)interaction
878 willDismissMenuForConfiguration:(UIEditMenuConfiguration*)configuration
879 animator:(
id<UIEditMenuInteractionAnimating>)animator
880 API_AVAILABLE(ios(16.0)) {
881 [
self.textInputDelegate flutterTextInputView:self
882 willDismissEditMenuWithTextInputClient:_textInputClient];
885 - (CGRect)editMenuInteraction:(UIEditMenuInteraction*)interaction
886 targetRectForConfiguration:(UIEditMenuConfiguration*)configuration API_AVAILABLE(ios(16.0)) {
887 return _editMenuTargetRect;
890 - (void)showEditMenuWithTargetRect:(CGRect)targetRect API_AVAILABLE(ios(16.0)) {
891 _editMenuTargetRect = targetRect;
892 UIEditMenuConfiguration* config =
893 [UIEditMenuConfiguration configurationWithIdentifier:nil sourcePoint:CGPointZero];
894 [
self.editMenuInteraction presentEditMenuWithConfiguration:config];
898 [
self.editMenuInteraction dismissMenu];
901 - (void)configureWithDictionary:(NSDictionary*)configuration {
902 NSDictionary* inputType = configuration[kKeyboardType];
904 NSDictionary* autofill = configuration[kAutofillProperties];
906 self.secureTextEntry = [configuration[kSecureTextEntry] boolValue];
907 self.enableDeltaModel = [configuration[kEnableDeltaModel] boolValue];
914 NSString* smartDashesType = configuration[kSmartDashesType];
916 bool smartDashesIsDisabled = smartDashesType && [smartDashesType isEqualToString:@"0"];
917 self.smartDashesType = smartDashesIsDisabled ? UITextSmartDashesTypeNo : UITextSmartDashesTypeYes;
918 NSString* smartQuotesType = configuration[kSmartQuotesType];
920 bool smartQuotesIsDisabled = smartQuotesType && [smartQuotesType isEqualToString:@"0"];
921 self.smartQuotesType = smartQuotesIsDisabled ? UITextSmartQuotesTypeNo : UITextSmartQuotesTypeYes;
923 self.keyboardAppearance = UIKeyboardAppearanceDark;
925 self.keyboardAppearance = UIKeyboardAppearanceLight;
927 self.keyboardAppearance = UIKeyboardAppearanceDefault;
929 NSString* autocorrect = configuration[kAutocorrectionType];
930 bool autocorrectIsDisabled = autocorrect && ![autocorrect boolValue];
931 self.autocorrectionType =
932 autocorrectIsDisabled ? UITextAutocorrectionTypeNo : UITextAutocorrectionTypeDefault;
933 self.spellCheckingType =
934 autocorrectIsDisabled ? UITextSpellCheckingTypeNo : UITextSpellCheckingTypeDefault;
936 if (autofill == nil) {
937 self.textContentType =
@"";
940 [
self setTextInputState:autofill[kAutofillEditingValue]];
941 NSAssert(_autofillId,
@"The autofill configuration must contain an autofill id");
945 self.isVisibleToAutofill = autofill || _secureTextEntry;
948 - (UITextContentType)textContentType {
949 return _textContentType;
962 - (UIColor*)insertionPointColor {
963 return [UIColor clearColor];
966 - (UIColor*)selectionBarColor {
967 return [UIColor clearColor];
970 - (UIColor*)selectionHighlightColor {
971 return [UIColor clearColor];
974 - (UIInputViewController*)inputViewController {
989 - (BOOL)respondsToSelector:(
SEL)selector {
990 if (@available(iOS 17.0, *)) {
992 if (selector ==
@selector(insertionPointColor)) {
996 return [
super respondsToSelector:selector];
999 - (void)setTextInputClient:(
int)client {
1000 _textInputClient = client;
1004 - (UITextInteraction*)textInteraction
API_AVAILABLE(ios(13.0)) {
1005 if (!_textInteraction) {
1006 _textInteraction = [UITextInteraction textInteractionForMode:UITextInteractionModeEditable];
1007 _textInteraction.textInput =
self;
1009 return _textInteraction;
1012 - (void)setTextInputState:(NSDictionary*)state {
1013 if (@available(iOS 13.0, *)) {
1020 [
self addInteraction:self.textInteraction];
1024 NSString* newText = state[@"text"];
1025 BOOL textChanged = ![
self.text isEqualToString:newText];
1027 [
self.inputDelegate textWillChange:self];
1028 [
self.text setString:newText];
1030 NSInteger composingBase = [state[@"composingBase"] intValue];
1031 NSInteger composingExtent = [state[@"composingExtent"] intValue];
1032 NSRange composingRange = [
self clampSelection:NSMakeRange(MIN(composingBase, composingExtent),
1033 ABS(composingBase - composingExtent))
1036 self.markedTextRange =
1039 NSRange selectedRange = [
self clampSelectionFromBase:[state[@"selectionBase"] intValue]
1040 extent:[state[@"selectionExtent"] intValue]
1043 NSRange oldSelectedRange = [(
FlutterTextRange*)
self.selectedTextRange range];
1044 if (!NSEqualRanges(selectedRange, oldSelectedRange)) {
1045 [
self.inputDelegate selectionWillChange:self];
1053 [
self.inputDelegate selectionDidChange:self];
1057 [
self.inputDelegate textDidChange:self];
1060 if (@available(iOS 13.0, *)) {
1061 if (_textInteraction) {
1062 [
self removeInteraction:_textInteraction];
1068 - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
1069 _scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
1070 [
self resetScribbleInteractionStatusIfEnding];
1071 [
self.viewResponder touchesBegan:touches withEvent:event];
1074 - (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {
1075 [
self.viewResponder touchesMoved:touches withEvent:event];
1078 - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
1079 [
self.viewResponder touchesEnded:touches withEvent:event];
1082 - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
1083 [
self.viewResponder touchesCancelled:touches withEvent:event];
1086 - (void)touchesEstimatedPropertiesUpdated:(NSSet*)touches {
1087 [
self.viewResponder touchesEstimatedPropertiesUpdated:touches];
1097 - (NSRange)clampSelectionFromBase:(
int)selectionBase
1098 extent:(
int)selectionExtent
1099 forText:(NSString*)text {
1100 int loc = MIN(selectionBase, selectionExtent);
1101 int len = ABS(selectionExtent - selectionBase);
1102 return loc < 0 ? NSMakeRange(0, 0)
1103 : [
self clampSelection:NSMakeRange(loc, len) forText:
self.text];
1106 - (NSRange)clampSelection:(NSRange)range forText:(NSString*)text {
1107 NSUInteger start = MIN(MAX(range.location, 0), text.length);
1108 NSUInteger length = MIN(range.length, text.length - start);
1109 return NSMakeRange(start, length);
1112 - (BOOL)isVisibleToAutofill {
1113 return self.frame.size.width > 0 &&
self.frame.size.height > 0;
1121 - (void)setIsVisibleToAutofill:(BOOL)isVisibleToAutofill {
1124 self.frame = isVisibleToAutofill ? CGRectMake(0, 0, 1, 1) : CGRectZero;
1127 #pragma mark UIScribbleInteractionDelegate
1132 if (@available(iOS 14.0, *)) {
1133 if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
1140 - (void)scribbleInteractionWillBeginWriting:(UIScribbleInteraction*)interaction
1141 API_AVAILABLE(ios(14.0)) {
1143 [
self.textInputDelegate flutterTextInputViewScribbleInteractionBegan:self];
1146 - (void)scribbleInteractionDidFinishWriting:(UIScribbleInteraction*)interaction
1147 API_AVAILABLE(ios(14.0)) {
1149 [
self.textInputDelegate flutterTextInputViewScribbleInteractionFinished:self];
1152 - (BOOL)scribbleInteraction:(UIScribbleInteraction*)interaction
1153 shouldBeginAtLocation:(CGPoint)location API_AVAILABLE(ios(14.0)) {
1157 - (BOOL)scribbleInteractionShouldDelayFocus:(UIScribbleInteraction*)interaction
1158 API_AVAILABLE(ios(14.0)) {
1162 #pragma mark - UIResponder Overrides
1164 - (BOOL)canBecomeFirstResponder {
1169 return _textInputClient != 0;
1172 - (BOOL)resignFirstResponder {
1173 BOOL success = [
super resignFirstResponder];
1175 if (!_preventCursorDismissWhenResignFirstResponder) {
1176 [
self.textInputDelegate flutterTextInputView:self
1177 didResignFirstResponderWithTextInputClient:_textInputClient];
1183 - (BOOL)canPerformAction:(
SEL)action withSender:(
id)sender {
1184 if (action ==
@selector(paste:)) {
1186 return [UIPasteboard generalPasteboard].hasStrings;
1187 }
else if (action ==
@selector(copy:) || action ==
@selector(cut:) ||
1188 action ==
@selector(
delete:)) {
1189 return [
self textInRange:_selectedTextRange].length > 0;
1190 }
else if (action ==
@selector(selectAll:)) {
1191 return self.hasText;
1193 return [
super canPerformAction:action withSender:sender];
1196 #pragma mark - UIResponderStandardEditActions Overrides
1198 - (void)cut:(
id)sender {
1199 [UIPasteboard generalPasteboard].string = [
self textInRange:_selectedTextRange];
1200 [
self replaceRange:_selectedTextRange withText:@""];
1203 - (void)copy:(
id)sender {
1204 [UIPasteboard generalPasteboard].string = [
self textInRange:_selectedTextRange];
1207 - (void)paste:(
id)sender {
1208 NSString* pasteboardString = [UIPasteboard generalPasteboard].string;
1209 if (pasteboardString != nil) {
1210 [
self insertText:pasteboardString];
1214 - (void)delete:(
id)sender {
1215 [
self replaceRange:_selectedTextRange withText:@""];
1218 - (void)selectAll:(
id)sender {
1219 [
self setSelectedTextRange:[
self textRangeFromPosition:[
self beginningOfDocument]
1220 toPosition:[
self endOfDocument]]];
1223 #pragma mark - UITextInput Overrides
1225 - (id<UITextInputTokenizer>)tokenizer {
1226 if (_tokenizer == nil) {
1233 return [_selectedTextRange copy];
1237 - (void)setSelectedTextRangeLocal:(UITextRange*)selectedTextRange {
1242 rangeWithNSRange:fml::RangeForCharactersInRange(self.text, flutterTextRange.range)] copy];
1249 - (void)setSelectedTextRange:(UITextRange*)selectedTextRange {
1254 [
self setSelectedTextRangeLocal:selectedTextRange];
1256 if (_enableDeltaModel) {
1257 [
self updateEditingStateWithDelta:flutter::TextEditingDelta([
self.text UTF8String])];
1259 [
self updateEditingState];
1263 _scribbleFocusStatus == FlutterScribbleFocusStatusFocused) {
1267 if (flutterTextRange.
range.length > 0) {
1268 [
self.textInputDelegate flutterTextInputView:self showToolbar:_textInputClient];
1272 [
self resetScribbleInteractionStatusIfEnding];
1275 - (id)insertDictationResultPlaceholder {
1279 - (void)removeDictationResultPlaceholder:(
id)placeholder willInsertResult:(BOOL)willInsertResult {
1282 - (NSString*)textInRange:(UITextRange*)range {
1287 @"Expected a FlutterTextRange for range (got %@).", [range
class]);
1289 NSAssert(textRange.location != NSNotFound,
@"Expected a valid text range.");
1291 NSUInteger location = MIN(textRange.location,
self.text.length);
1292 NSUInteger length = MIN(
self.text.length - location, textRange.length);
1293 NSRange safeRange = NSMakeRange(location, length);
1294 return [
self.text substringWithRange:safeRange];
1299 - (void)replaceRangeLocal:(NSRange)range withText:(NSString*)text {
1300 [
self.text replaceCharactersInRange:[
self clampSelection:range forText:self.text]
1306 const NSRange newSelectionRange =
1307 [
self clampSelection:NSMakeRange(range.location + text.length, 0) forText:self.text];
1310 self.markedTextRange = nil;
1313 - (void)replaceRange:(UITextRange*)range withText:(NSString*)text {
1314 NSString* textBeforeChange = [
self.text copy];
1316 [
self replaceRangeLocal:replaceRange withText:text];
1317 if (_enableDeltaModel) {
1318 NSRange nextReplaceRange = [
self clampSelection:replaceRange forText:textBeforeChange];
1319 [
self updateEditingStateWithDelta:flutter::TextEditingDelta(
1320 [textBeforeChange UTF8String],
1322 nextReplaceRange.location,
1323 nextReplaceRange.location + nextReplaceRange.length),
1324 [text UTF8String])];
1326 [
self updateEditingState];
1330 - (BOOL)shouldChangeTextInRange:(UITextRange*)range replacementText:(NSString*)text {
1333 self.temporarilyDeletedComposedCharacter = nil;
1335 if (
self.
returnKeyType == UIReturnKeyDefault && [text isEqualToString:
@"\n"]) {
1336 [
self.textInputDelegate flutterTextInputView:self
1337 performAction:FlutterTextInputActionNewline
1338 withClient:_textInputClient];
1342 if ([text isEqualToString:
@"\n"]) {
1343 FlutterTextInputAction action;
1345 case UIReturnKeyDefault:
1346 action = FlutterTextInputActionUnspecified;
1348 case UIReturnKeyDone:
1349 action = FlutterTextInputActionDone;
1352 action = FlutterTextInputActionGo;
1354 case UIReturnKeySend:
1355 action = FlutterTextInputActionSend;
1357 case UIReturnKeySearch:
1358 case UIReturnKeyGoogle:
1359 case UIReturnKeyYahoo:
1360 action = FlutterTextInputActionSearch;
1362 case UIReturnKeyNext:
1363 action = FlutterTextInputActionNext;
1365 case UIReturnKeyContinue:
1366 action = FlutterTextInputActionContinue;
1368 case UIReturnKeyJoin:
1369 action = FlutterTextInputActionJoin;
1371 case UIReturnKeyRoute:
1372 action = FlutterTextInputActionRoute;
1374 case UIReturnKeyEmergencyCall:
1375 action = FlutterTextInputActionEmergencyCall;
1379 [
self.textInputDelegate flutterTextInputView:self
1380 performAction:action
1381 withClient:_textInputClient];
1390 - (void)setMarkedText:(NSString*)markedText selectedRange:(NSRange)markedSelectedRange {
1391 NSString* textBeforeChange = [
self.text copy];
1394 _scribbleFocusStatus != FlutterScribbleFocusStatusUnfocused) {
1398 if (markedText == nil) {
1403 const NSRange& actualReplacedRange = currentMarkedTextRange && !currentMarkedTextRange.isEmpty
1404 ? currentMarkedTextRange.
range
1408 [
self.text replaceCharactersInRange:actualReplacedRange withString:markedText];
1410 const NSRange newMarkedRange = NSMakeRange(actualReplacedRange.location, markedText.length);
1411 self.markedTextRange =
1414 [
self setSelectedTextRangeLocal:
1416 rangeWithNSRange:[
self clampSelection:NSMakeRange(markedSelectedRange.location +
1417 newMarkedRange.location,
1418 markedSelectedRange.length)
1419 forText:self.text]]];
1420 if (_enableDeltaModel) {
1421 NSRange nextReplaceRange = [
self clampSelection:actualReplacedRange forText:textBeforeChange];
1422 [
self updateEditingStateWithDelta:flutter::TextEditingDelta(
1423 [textBeforeChange UTF8String],
1425 nextReplaceRange.location,
1426 nextReplaceRange.location + nextReplaceRange.length),
1427 [markedText UTF8String])];
1429 [
self updateEditingState];
1433 - (void)unmarkText {
1437 self.markedTextRange = nil;
1438 if (_enableDeltaModel) {
1439 [
self updateEditingStateWithDelta:flutter::TextEditingDelta([
self.text UTF8String])];
1441 [
self updateEditingState];
1445 - (UITextRange*)textRangeFromPosition:(UITextPosition*)fromPosition
1446 toPosition:(UITextPosition*)toPosition {
1449 if (toIndex >= fromIndex) {
1462 - (NSUInteger)decrementOffsetPosition:(NSUInteger)position {
1463 return fml::RangeForCharacterAtIndex(
self.text, MAX(0, position - 1)).location;
1466 - (NSUInteger)incrementOffsetPosition:(NSUInteger)position {
1467 NSRange charRange = fml::RangeForCharacterAtIndex(
self.text, position);
1468 return MIN(position + charRange.length,
self.text.length);
1471 - (UITextPosition*)positionFromPosition:(UITextPosition*)position offset:(NSInteger)offset {
1474 NSInteger newLocation = (NSInteger)offsetPosition + offset;
1475 if (newLocation < 0 || newLocation > (NSInteger)
self.text.length) {
1484 for (NSInteger i = 0; i < offset && offsetPosition <
self.text.length; ++i) {
1485 offsetPosition = [
self incrementOffsetPosition:offsetPosition];
1488 for (NSInteger i = 0; i < ABS(offset) && offsetPosition > 0; ++i) {
1489 offsetPosition = [
self decrementOffsetPosition:offsetPosition];
1495 - (UITextPosition*)positionFromPosition:(UITextPosition*)position
1496 inDirection:(UITextLayoutDirection)direction
1497 offset:(NSInteger)offset {
1499 switch (direction) {
1500 case UITextLayoutDirectionLeft:
1501 case UITextLayoutDirectionUp:
1502 return [
self positionFromPosition:position offset:offset * -1];
1503 case UITextLayoutDirectionRight:
1504 case UITextLayoutDirectionDown:
1505 return [
self positionFromPosition:position offset:1];
1509 - (UITextPosition*)beginningOfDocument {
1513 - (UITextPosition*)endOfDocument {
1515 affinity:UITextStorageDirectionBackward];
1518 - (NSComparisonResult)comparePosition:(UITextPosition*)position toPosition:(UITextPosition*)other {
1521 if (positionIndex < otherIndex) {
1522 return NSOrderedAscending;
1524 if (positionIndex > otherIndex) {
1525 return NSOrderedDescending;
1529 if (positionAffinity == otherAffinity) {
1530 return NSOrderedSame;
1532 if (positionAffinity == UITextStorageDirectionBackward) {
1534 return NSOrderedAscending;
1537 return NSOrderedDescending;
1540 - (NSInteger)offsetFromPosition:(UITextPosition*)from toPosition:(UITextPosition*)toPosition {
1544 - (UITextPosition*)positionWithinRange:(UITextRange*)range
1545 farthestInDirection:(UITextLayoutDirection)direction {
1547 UITextStorageDirection affinity;
1548 switch (direction) {
1549 case UITextLayoutDirectionLeft:
1550 case UITextLayoutDirectionUp:
1552 affinity = UITextStorageDirectionForward;
1554 case UITextLayoutDirectionRight:
1555 case UITextLayoutDirectionDown:
1557 affinity = UITextStorageDirectionBackward;
1563 - (UITextRange*)characterRangeByExtendingPosition:(UITextPosition*)position
1564 inDirection:(UITextLayoutDirection)direction {
1566 NSUInteger startIndex;
1567 NSUInteger endIndex;
1568 switch (direction) {
1569 case UITextLayoutDirectionLeft:
1570 case UITextLayoutDirectionUp:
1571 startIndex = [
self decrementOffsetPosition:positionIndex];
1572 endIndex = positionIndex;
1574 case UITextLayoutDirectionRight:
1575 case UITextLayoutDirectionDown:
1576 startIndex = positionIndex;
1577 endIndex = [
self incrementOffsetPosition:positionIndex];
1583 #pragma mark - UITextInput text direction handling
1585 - (UITextWritingDirection)baseWritingDirectionForPosition:(UITextPosition*)position
1586 inDirection:(UITextStorageDirection)direction {
1588 return UITextWritingDirectionNatural;
1591 - (void)setBaseWritingDirection:(UITextWritingDirection)writingDirection
1592 forRange:(UITextRange*)range {
1596 #pragma mark - UITextInput cursor, selection rect handling
1598 - (void)setMarkedRect:(CGRect)markedRect {
1599 _markedRect = markedRect;
1606 - (void)setEditableTransform:(NSArray*)matrix {
1607 CATransform3D* transform = &_editableTransform;
1609 transform->m11 = [matrix[0] doubleValue];
1610 transform->m12 = [matrix[1] doubleValue];
1611 transform->m13 = [matrix[2] doubleValue];
1612 transform->m14 = [matrix[3] doubleValue];
1614 transform->m21 = [matrix[4] doubleValue];
1615 transform->m22 = [matrix[5] doubleValue];
1616 transform->m23 = [matrix[6] doubleValue];
1617 transform->m24 = [matrix[7] doubleValue];
1619 transform->m31 = [matrix[8] doubleValue];
1620 transform->m32 = [matrix[9] doubleValue];
1621 transform->m33 = [matrix[10] doubleValue];
1622 transform->m34 = [matrix[11] doubleValue];
1624 transform->m41 = [matrix[12] doubleValue];
1625 transform->m42 = [matrix[13] doubleValue];
1626 transform->m43 = [matrix[14] doubleValue];
1627 transform->m44 = [matrix[15] doubleValue];
1636 CGPoint points[] = {
1637 incomingRect.origin,
1638 CGPointMake(incomingRect.origin.x, incomingRect.origin.y + incomingRect.size.height),
1639 CGPointMake(incomingRect.origin.x + incomingRect.size.width, incomingRect.origin.y),
1640 CGPointMake(incomingRect.origin.x + incomingRect.size.width,
1641 incomingRect.origin.y + incomingRect.size.height)};
1643 CGPoint origin = CGPointMake(CGFLOAT_MAX, CGFLOAT_MAX);
1644 CGPoint farthest = CGPointMake(-CGFLOAT_MAX, -CGFLOAT_MAX);
1646 for (
int i = 0; i < 4; i++) {
1647 const CGPoint point = points[i];
1649 CGFloat x = _editableTransform.m11 * point.x + _editableTransform.m21 * point.y +
1650 _editableTransform.m41;
1651 CGFloat y = _editableTransform.m12 * point.x + _editableTransform.m22 * point.y +
1652 _editableTransform.m42;
1654 const CGFloat w = _editableTransform.m14 * point.x + _editableTransform.m24 * point.y +
1655 _editableTransform.m44;
1659 }
else if (w != 1.0) {
1664 origin.x = MIN(origin.x, x);
1665 origin.y = MIN(origin.y, y);
1666 farthest.x = MAX(farthest.x, x);
1667 farthest.y = MAX(farthest.y, y);
1669 return CGRectMake(origin.x, origin.y, farthest.x - origin.x, farthest.y - origin.y);
1678 - (CGRect)firstRectForRange:(UITextRange*)range {
1680 @"Expected a FlutterTextPosition for range.start (got %@).", [range.start
class]);
1682 @"Expected a FlutterTextPosition for range.end (got %@).", [range.end
class]);
1685 if (_markedTextRange != nil) {
1696 CGRect rect = _markedRect;
1697 if (CGRectIsEmpty(rect)) {
1698 rect = CGRectInset(rect, -0.1, 0);
1704 NSAssert(hostView == nil || [
self isDescendantOfView:hostView],
@"%@ is not a descendant of %@",
1706 return hostView ? [hostView convertRect:_cachedFirstRect toView:self] :
_cachedFirstRect;
1710 _scribbleFocusStatus == FlutterScribbleFocusStatusUnfocused) {
1711 if (@available(iOS 17.0, *)) {
1721 [
self.textInputDelegate flutterTextInputView:self
1722 showAutocorrectionPromptRectForStart:start
1724 withClient:_textInputClient];
1732 if (@available(iOS 17, *)) {
1738 NSUInteger first = start;
1743 CGRect startSelectionRect = CGRectNull;
1744 CGRect endSelectionRect = CGRectNull;
1747 CGFloat minY = CGFLOAT_MAX;
1748 CGFloat maxY = CGFLOAT_MIN;
1751 rangeWithNSRange:fml::RangeForCharactersInRange(self.text, NSMakeRange(0, self.text.length))];
1752 for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
1753 BOOL startsOnOrBeforeStartOfRange = _selectionRects[i].position <= first;
1754 BOOL isLastSelectionRect = i + 1 == [_selectionRects count];
1755 BOOL endOfTextIsAfterStartOfRange = isLastSelectionRect && textRange.
range.length > first;
1756 BOOL nextSelectionRectIsAfterStartOfRange =
1757 !isLastSelectionRect && _selectionRects[i + 1].position > first;
1758 if (startsOnOrBeforeStartOfRange &&
1759 (endOfTextIsAfterStartOfRange || nextSelectionRectIsAfterStartOfRange)) {
1761 if (@available(iOS 17, *)) {
1762 startSelectionRect = _selectionRects[i].rect;
1764 return _selectionRects[i].rect;
1767 if (!CGRectIsNull(startSelectionRect)) {
1768 minY = fmin(minY, CGRectGetMinY(_selectionRects[i].rect));
1769 maxY = fmax(maxY, CGRectGetMaxY(_selectionRects[i].rect));
1770 BOOL endsOnOrAfterEndOfRange = _selectionRects[i].position >= end - 1;
1771 BOOL nextSelectionRectIsOnNextLine =
1772 !isLastSelectionRect &&
1777 CGRectGetMidY(_selectionRects[i + 1].rect) > CGRectGetMaxY(_selectionRects[i].rect);
1778 if (endsOnOrAfterEndOfRange || isLastSelectionRect || nextSelectionRectIsOnNextLine) {
1779 endSelectionRect = _selectionRects[i].rect;
1784 if (CGRectIsNull(startSelectionRect) || CGRectIsNull(endSelectionRect)) {
1788 CGFloat minX = fmin(CGRectGetMinX(startSelectionRect), CGRectGetMinX(endSelectionRect));
1789 CGFloat maxX = fmax(CGRectGetMaxX(startSelectionRect), CGRectGetMaxX(endSelectionRect));
1790 return CGRectMake(minX, minY, maxX - minX, maxY - minY);
1798 NSArray<UITextSelectionRect*>* rects = [
self
1800 rangeWithNSRange:fml::RangeForCharactersInRange(
1804 (index >= (NSInteger)self.text.length)
1807 if (rects.count == 0) {
1813 CGRect characterAfterCaret = rects[0].rect;
1818 return CGRectMake(characterAfterCaret.origin.x + characterAfterCaret.size.width,
1819 characterAfterCaret.origin.y, 0, characterAfterCaret.size.height);
1821 return CGRectMake(characterAfterCaret.origin.x, characterAfterCaret.origin.y, 0,
1822 characterAfterCaret.size.height);
1824 }
else if (rects.count == 2 && affinity == UITextStorageDirectionForward) {
1827 CGRect characterAfterCaret = rects[1].rect;
1832 return CGRectMake(characterAfterCaret.origin.x + characterAfterCaret.size.width,
1833 characterAfterCaret.origin.y, 0, characterAfterCaret.size.height);
1835 return CGRectMake(characterAfterCaret.origin.x, characterAfterCaret.origin.y, 0,
1836 characterAfterCaret.size.height);
1845 CGRect characterBeforeCaret = rects[0].rect;
1848 return CGRectMake(characterBeforeCaret.origin.x, characterBeforeCaret.origin.y, 0,
1849 characterBeforeCaret.size.height);
1851 return CGRectMake(characterBeforeCaret.origin.x + characterBeforeCaret.size.width,
1852 characterBeforeCaret.origin.y, 0, characterBeforeCaret.size.height);
1856 - (UITextPosition*)closestPositionToPoint:(CGPoint)point {
1857 if ([_selectionRects count] == 0) {
1859 @"Expected a FlutterTextPosition for position (got %@).",
1862 UITextStorageDirection currentAffinity =
1868 rangeWithNSRange:fml::RangeForCharactersInRange(self.text, NSMakeRange(0, self.text.length))];
1869 return [
self closestPositionToPoint:point withinRange:range];
1872 - (NSArray*)selectionRectsForRange:(UITextRange*)range {
1880 @"Expected a FlutterTextPosition for range.start (got %@).", [range.start
class]);
1882 @"Expected a FlutterTextPosition for range.end (got %@).", [range.end
class]);
1885 NSMutableArray* rects = [[NSMutableArray alloc] init];
1886 for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
1887 if (_selectionRects[i].position >= start &&
1888 (_selectionRects[i].position < end ||
1889 (start == end && _selectionRects[i].position <= end))) {
1890 float width = _selectionRects[i].rect.size.width;
1894 CGRect rect = CGRectMake(_selectionRects[i].rect.origin.x, _selectionRects[i].rect.origin.y,
1895 width, _selectionRects[i].rect.size.height);
1898 position:_selectionRects[i].position
1902 self.text, NSMakeRange(0, self.text.length))
1905 [rects addObject:selectionRect];
1911 - (UITextPosition*)closestPositionToPoint:(CGPoint)point withinRange:(UITextRange*)range {
1913 @"Expected a FlutterTextPosition for range.start (got %@).", [range.start
class]);
1915 @"Expected a FlutterTextPosition for range.end (got %@).", [range.end
class]);
1925 NSUInteger _closestRectIndex = 0;
1926 for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
1927 NSUInteger position = _selectionRects[i].position;
1928 if (position >= start && position <= end) {
1931 point, _selectionRects[i].rect, _selectionRects[i].isRTL,
1932 NO, _selectionRects[_closestRectIndex].rect,
1933 _selectionRects[_closestRectIndex].isRTL, verticalPrecision)) {
1935 _closestRectIndex = i;
1942 affinity:UITextStorageDirectionForward];
1948 for (NSUInteger i = MAX(0, _closestRectIndex - 1);
1949 i < MIN(_closestRectIndex + 2, [_selectionRects count]); i++) {
1950 NSUInteger position = _selectionRects[i].position + 1;
1951 if (position >= start && position <= end) {
1953 point, _selectionRects[i].rect, _selectionRects[i].isRTL,
1954 YES, _selectionRects[_closestRectIndex].rect,
1955 _selectionRects[_closestRectIndex].isRTL, verticalPrecision)) {
1958 affinity:UITextStorageDirectionBackward];
1963 return closestPosition;
1966 - (UITextRange*)characterRangeAtPoint:(CGPoint)point {
1969 return [
FlutterTextRange rangeWithNSRange:fml::RangeForCharacterAtIndex(self.text, currentIndex)];
2000 - (void)beginFloatingCursorAtPoint:(CGPoint)point {
2017 [
self.textInputDelegate flutterTextInputView:self
2018 updateFloatingCursor:FlutterFloatingCursorDragStateStart
2019 withClient:_textInputClient
2020 withPosition:@{@"X" : @0, @"Y" : @0}];
2023 - (void)updateFloatingCursorAtPoint:(CGPoint)point {
2024 [
self.textInputDelegate flutterTextInputView:self
2025 updateFloatingCursor:FlutterFloatingCursorDragStateUpdate
2026 withClient:_textInputClient
2028 @"X" : @(point.x - _floatingCursorOffset.x),
2029 @"Y" : @(point.y - _floatingCursorOffset.y)
2033 - (void)endFloatingCursor {
2035 [
self.textInputDelegate flutterTextInputView:self
2036 updateFloatingCursor:FlutterFloatingCursorDragStateEnd
2037 withClient:_textInputClient
2038 withPosition:@{@"X" : @0, @"Y" : @0}];
2041 #pragma mark - UIKeyInput Overrides
2043 - (void)updateEditingState {
2048 NSInteger composingBase = -1;
2049 NSInteger composingExtent = -1;
2054 NSDictionary* state = @{
2055 @"selectionBase" : @(selectionBase),
2056 @"selectionExtent" : @(selectionExtent),
2058 @"selectionIsDirectional" : @(
false),
2059 @"composingBase" : @(composingBase),
2060 @"composingExtent" : @(composingExtent),
2061 @"text" : [NSString stringWithString:self.text],
2064 if (_textInputClient == 0 && _autofillId != nil) {
2065 [
self.textInputDelegate flutterTextInputView:self
2066 updateEditingClient:_textInputClient
2068 withTag:_autofillId];
2070 [
self.textInputDelegate flutterTextInputView:self
2071 updateEditingClient:_textInputClient
2076 - (void)updateEditingStateWithDelta:(
flutter::TextEditingDelta)delta {
2081 NSInteger composingBase = -1;
2082 NSInteger composingExtent = -1;
2088 NSDictionary* deltaToFramework = @{
2089 @"oldText" : @(delta.old_text().c_str()),
2090 @"deltaText" : @(delta.delta_text().c_str()),
2091 @"deltaStart" : @(delta.delta_start()),
2092 @"deltaEnd" : @(delta.delta_end()),
2093 @"selectionBase" : @(selectionBase),
2094 @"selectionExtent" : @(selectionExtent),
2096 @"selectionIsDirectional" : @(
false),
2097 @"composingBase" : @(composingBase),
2098 @"composingExtent" : @(composingExtent),
2101 [_pendingDeltas addObject:deltaToFramework];
2103 if (_pendingDeltas.count == 1) {
2105 dispatch_async(dispatch_get_main_queue(), ^{
2107 if (strongSelf && strongSelf.pendingDeltas.count > 0) {
2108 NSDictionary* deltas = @{
2109 @"deltas" : strongSelf.pendingDeltas,
2112 [strongSelf.textInputDelegate flutterTextInputView:strongSelf
2113 updateEditingClient:strongSelf->_textInputClient
2115 [strongSelf.pendingDeltas removeAllObjects];
2122 return self.text.length > 0;
2125 - (void)insertText:(NSString*)text {
2126 if (
self.temporarilyDeletedComposedCharacter.length > 0 && text.length == 1 && !text.UTF8String &&
2127 [text characterAtIndex:0] == [
self.temporarilyDeletedComposedCharacter characterAtIndex:0]) {
2131 text =
self.temporarilyDeletedComposedCharacter;
2132 self.temporarilyDeletedComposedCharacter = nil;
2135 NSMutableArray<FlutterTextSelectionRect*>* copiedRects =
2136 [[NSMutableArray alloc] initWithCapacity:[_selectionRects count]];
2138 @"Expected a FlutterTextPosition for position (got %@).",
2141 for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
2142 NSUInteger rectPosition = _selectionRects[i].position;
2143 if (rectPosition == insertPosition) {
2144 for (NSUInteger j = 0; j <= text.length; j++) {
2151 if (rectPosition > insertPosition) {
2152 rectPosition = rectPosition + text.length;
2161 _scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
2162 [
self resetScribbleInteractionStatusIfEnding];
2163 self.selectionRects = copiedRects;
2165 [
self replaceRange:_selectedTextRange withText:text];
2168 - (UITextPlaceholder*)insertTextPlaceholderWithSize:(CGSize)size API_AVAILABLE(ios(13.0)) {
2169 [
self.textInputDelegate flutterTextInputView:self
2170 insertTextPlaceholderWithSize:size
2171 withClient:_textInputClient];
2176 - (void)removeTextPlaceholder:(UITextPlaceholder*)textPlaceholder API_AVAILABLE(ios(13.0)) {
2178 [
self.textInputDelegate flutterTextInputView:self removeTextPlaceholder:_textInputClient];
2181 - (void)deleteBackward {
2183 _scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
2184 [
self resetScribbleInteractionStatusIfEnding];
2201 if (oldRange.location > 0) {
2202 NSRange newRange = NSMakeRange(oldRange.location - 1, 1);
2206 NSRange charRange = fml::RangeForCharacterAtIndex(
self.text, oldRange.location - 1);
2207 if (
IsEmoji(
self.text, charRange)) {
2208 newRange = NSMakeRange(charRange.location, oldRange.location - charRange.location);
2220 NSString* deletedText = [
self.text substringWithRange:_selectedTextRange.range];
2221 NSRange deleteFirstCharacterRange = fml::RangeForCharacterAtIndex(deletedText, 0);
2222 self.temporarilyDeletedComposedCharacter =
2223 [deletedText substringWithRange:deleteFirstCharacterRange];
2225 [
self replaceRange:_selectedTextRange withText:@""];
2229 - (void)postAccessibilityNotification:(UIAccessibilityNotifications)notification target:(
id)target {
2230 UIAccessibilityPostNotification(notification, target);
2233 - (void)accessibilityElementDidBecomeFocused {
2234 if ([
self accessibilityElementIsFocused]) {
2238 FML_DCHECK(_backingTextInputAccessibilityObject);
2239 [
self postAccessibilityNotification:UIAccessibilityScreenChangedNotification
2240 target:_backingTextInputAccessibilityObject];
2244 - (BOOL)accessibilityElementsHidden {
2245 return !_accessibilityEnabled;
2254 #pragma mark - Key Events Handling
2255 - (void)pressesBegan:(NSSet<UIPress*>*)presses
2256 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2257 [_textInputPlugin.viewController pressesBegan:presses withEvent:event];
2260 - (void)pressesChanged:(NSSet<UIPress*>*)presses
2261 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2262 [_textInputPlugin.viewController pressesChanged:presses withEvent:event];
2265 - (void)pressesEnded:(NSSet<UIPress*>*)presses
2266 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2267 [_textInputPlugin.viewController pressesEnded:presses withEvent:event];
2270 - (void)pressesCancelled:(NSSet<UIPress*>*)presses
2271 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2272 [_textInputPlugin.viewController pressesCancelled:presses withEvent:event];
2301 - (BOOL)accessibilityElementsHidden {
2308 - (void)enableActiveViewAccessibility;
2325 - (void)enableActiveViewAccessibility {
2326 [
self.target enableActiveViewAccessibility];
2333 @property(nonatomic, readonly)
2334 NSMutableDictionary<NSString*, FlutterTextInputView*>* autofillContext;
2339 @property(nonatomic, strong) UIView* keyboardViewContainer;
2340 @property(nonatomic, strong) UIView* keyboardView;
2341 @property(nonatomic, strong) UIView* cachedFirstResponder;
2342 @property(nonatomic, assign) CGRect keyboardRect;
2343 @property(nonatomic, assign) CGFloat previousPointerYPosition;
2344 @property(nonatomic, assign) CGFloat pointerYVelocity;
2348 NSTimer* _enableFlutterTextInputViewAccessibilityTimer;
2352 self = [
super init];
2355 _textInputDelegate = textInputDelegate;
2356 _autofillContext = [[NSMutableDictionary alloc] init];
2358 _scribbleElements = [[NSMutableDictionary alloc] init];
2359 _keyboardViewContainer = [[UIView alloc] init];
2361 [[NSNotificationCenter defaultCenter] addObserver:self
2362 selector:@selector(handleKeyboardWillShow:)
2363 name:UIKeyboardWillShowNotification
2370 - (void)handleKeyboardWillShow:(NSNotification*)notification {
2371 NSDictionary* keyboardInfo = [notification userInfo];
2372 NSValue* keyboardFrameEnd = [keyboardInfo valueForKey:UIKeyboardFrameEndUserInfoKey];
2373 _keyboardRect = [keyboardFrameEnd CGRectValue];
2377 [
self hideTextInput];
2380 - (void)removeEnableFlutterTextInputViewAccessibilityTimer {
2381 if (_enableFlutterTextInputViewAccessibilityTimer) {
2382 [_enableFlutterTextInputViewAccessibilityTimer invalidate];
2383 _enableFlutterTextInputViewAccessibilityTimer = nil;
2387 - (UIView<UITextInput>*)textInputView {
2392 NSString* method = call.
method;
2395 [
self showTextInput];
2397 }
else if ([method isEqualToString:
kHideMethod]) {
2398 [
self hideTextInput];
2401 [
self setTextInputClient:[args[0] intValue] withConfiguration:args[1]];
2405 [
self setPlatformViewTextInputClient];
2408 [
self setTextInputEditingState:args];
2411 [
self clearTextInputClient];
2414 [
self setEditableSizeAndTransform:args];
2417 [
self updateMarkedRect:args];
2420 [
self triggerAutofillSave:[args boolValue]];
2426 [
self setSelectionRects:args];
2429 [
self setSelectionRects:args];
2432 [
self startLiveTextInput];
2435 [
self updateConfig:args];
2438 CGFloat pointerY = (CGFloat)[args[
@"pointerY"] doubleValue];
2439 [
self handlePointerMove:pointerY];
2442 CGFloat pointerY = (CGFloat)[args[
@"pointerY"] doubleValue];
2443 [
self handlePointerUp:pointerY];
2450 - (void)handlePointerUp:(CGFloat)pointerY {
2451 if (_keyboardView.superview != nil) {
2455 CGFloat screenHeight = screen.bounds.size.height;
2456 CGFloat keyboardHeight = _keyboardRect.size.height;
2458 BOOL shouldDismissKeyboardBasedOnVelocity = _pointerYVelocity < 0;
2459 [UIView animateWithDuration:kKeyboardAnimationTimeToCompleteion
2461 double keyboardDestination =
2462 shouldDismissKeyboardBasedOnVelocity ? screenHeight : screenHeight - keyboardHeight;
2463 _keyboardViewContainer.frame = CGRectMake(
2464 0, keyboardDestination, _viewController.flutterScreenIfViewLoaded.bounds.size.width,
2465 _keyboardViewContainer.frame.size.height);
2467 completion:^(BOOL finished) {
2468 if (shouldDismissKeyboardBasedOnVelocity) {
2469 [
self.textInputDelegate flutterTextInputView:self.activeView
2470 didResignFirstResponderWithTextInputClient:self.activeView.textInputClient];
2471 [
self dismissKeyboardScreenshot];
2473 [
self showKeyboardAndRemoveScreenshot];
2479 - (void)dismissKeyboardScreenshot {
2480 for (UIView* subView in _keyboardViewContainer.subviews) {
2481 [subView removeFromSuperview];
2485 - (void)showKeyboardAndRemoveScreenshot {
2486 [UIView setAnimationsEnabled:NO];
2487 [_cachedFirstResponder becomeFirstResponder];
2491 dispatch_get_main_queue(), ^{
2492 [UIView setAnimationsEnabled:YES];
2493 [
self dismissKeyboardScreenshot];
2497 - (void)handlePointerMove:(CGFloat)pointerY {
2500 CGFloat screenHeight = screen.bounds.size.height;
2501 CGFloat keyboardHeight = _keyboardRect.size.height;
2502 if (screenHeight - keyboardHeight <= pointerY) {
2504 if (_keyboardView.superview == nil) {
2506 [
self takeKeyboardScreenshotAndDisplay];
2507 [
self hideKeyboardWithoutAnimationAndAvoidCursorDismissUpdate];
2509 [
self setKeyboardContainerHeight:pointerY];
2510 _pointerYVelocity = _previousPointerYPosition - pointerY;
2513 if (_keyboardView.superview != nil) {
2515 _keyboardViewContainer.frame = _keyboardRect;
2516 _pointerYVelocity = _previousPointerYPosition - pointerY;
2519 _previousPointerYPosition = pointerY;
2522 - (void)setKeyboardContainerHeight:(CGFloat)pointerY {
2523 CGRect frameRect = _keyboardRect;
2524 frameRect.origin.y = pointerY;
2525 _keyboardViewContainer.frame = frameRect;
2528 - (void)hideKeyboardWithoutAnimationAndAvoidCursorDismissUpdate {
2529 [UIView setAnimationsEnabled:NO];
2530 _cachedFirstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder;
2531 _activeView.preventCursorDismissWhenResignFirstResponder = YES;
2532 [_cachedFirstResponder resignFirstResponder];
2533 _activeView.preventCursorDismissWhenResignFirstResponder = NO;
2534 [UIView setAnimationsEnabled:YES];
2537 - (void)takeKeyboardScreenshotAndDisplay {
2540 UIView* keyboardSnap = [screen snapshotViewAfterScreenUpdates:YES];
2541 keyboardSnap = [keyboardSnap resizableSnapshotViewFromRect:_keyboardRect
2542 afterScreenUpdates:YES
2543 withCapInsets:UIEdgeInsetsZero];
2544 _keyboardView = keyboardSnap;
2545 [_keyboardViewContainer addSubview:_keyboardView];
2546 if (_keyboardViewContainer.superview == nil) {
2547 [UIApplication.sharedApplication.delegate.window.rootViewController.view
2548 addSubview:_keyboardViewContainer];
2550 _keyboardViewContainer.layer.zPosition = NSIntegerMax;
2551 _keyboardViewContainer.frame = _keyboardRect;
2554 - (BOOL)showEditMenu:(NSDictionary*)args API_AVAILABLE(ios(16.0)) {
2555 if (!
self.activeView.isFirstResponder) {
2558 NSDictionary<NSString*, NSNumber*>* encodedTargetRect = args[@"targetRect"];
2559 CGRect globalTargetRect = CGRectMake(
2560 [encodedTargetRect[
@"x"] doubleValue], [encodedTargetRect[
@"y"] doubleValue],
2561 [encodedTargetRect[
@"width"] doubleValue], [encodedTargetRect[
@"height"] doubleValue]);
2562 CGRect localTargetRect = [
self.hostView convertRect:globalTargetRect toView:self.activeView];
2563 [
self.activeView showEditMenuWithTargetRect:localTargetRect];
2567 - (void)hideEditMenu {
2568 [
self.activeView hideEditMenu];
2571 - (void)setEditableSizeAndTransform:(NSDictionary*)dictionary {
2572 NSArray* transform = dictionary[@"transform"];
2573 [_activeView setEditableTransform:transform];
2574 const int leftIndex = 12;
2575 const int topIndex = 13;
2579 CGRectMake([transform[leftIndex] intValue], [transform[topIndex] intValue],
2580 [dictionary[
@"width"] intValue], [dictionary[
@"height"] intValue]);
2582 CGRectMake(0, 0, [dictionary[
@"width"] intValue], [dictionary[
@"height"] intValue]);
2583 _activeView.tintColor = [UIColor clearColor];
2588 if (@available(iOS 17, *)) {
2596 CGRectMake([transform[leftIndex] intValue], [transform[topIndex] intValue], 0, 0);
2601 - (void)updateMarkedRect:(NSDictionary*)dictionary {
2602 NSAssert(dictionary[
@"x"] != nil && dictionary[
@"y"] != nil && dictionary[
@"width"] != nil &&
2603 dictionary[
@"height"] != nil,
2604 @"Expected a dictionary representing a CGRect, got %@", dictionary);
2605 CGRect rect = CGRectMake([dictionary[
@"x"] doubleValue], [dictionary[
@"y"] doubleValue],
2606 [dictionary[
@"width"] doubleValue], [dictionary[
@"height"] doubleValue]);
2607 _activeView.markedRect = rect.size.width < 0 && rect.size.height < 0 ?
kInvalidFirstRect : rect;
2610 - (void)setSelectionRects:(NSArray*)encodedRects {
2611 NSMutableArray<FlutterTextSelectionRect*>* rectsAsRect =
2612 [[NSMutableArray alloc] initWithCapacity:[encodedRects count]];
2613 for (NSUInteger i = 0; i < [encodedRects count]; i++) {
2614 NSArray<NSNumber*>* encodedRect = encodedRects[i];
2616 selectionRectWithRect:CGRectMake([encodedRect[0] floatValue],
2617 [encodedRect[1] floatValue],
2618 [encodedRect[2] floatValue],
2619 [encodedRect[3] floatValue])
2620 position:[encodedRect[4] unsignedIntegerValue]
2621 writingDirection:[encodedRect[5] unsignedIntegerValue] == 1
2622 ? NSWritingDirectionLeftToRight
2623 : NSWritingDirectionRightToLeft]];
2629 _activeView.selectionRects = rectsAsRect;
2632 - (void)startLiveTextInput {
2633 if (@available(iOS 15.0, *)) {
2634 if (_activeView == nil || !_activeView.isFirstResponder) {
2637 [_activeView captureTextFromCamera:nil];
2641 - (void)showTextInput {
2642 _activeView.viewResponder = _viewResponder;
2643 [
self addToInputParentViewIfNeeded:_activeView];
2652 if (!_enableFlutterTextInputViewAccessibilityTimer) {
2653 _enableFlutterTextInputViewAccessibilityTimer =
2654 [NSTimer scheduledTimerWithTimeInterval:kUITextInputAccessibilityEnablingDelaySeconds
2656 selector:@selector(enableActiveViewAccessibility)
2660 [_activeView becomeFirstResponder];
2663 - (void)enableActiveViewAccessibility {
2664 if (_activeView.isFirstResponder) {
2665 _activeView.accessibilityEnabled = YES;
2667 [
self removeEnableFlutterTextInputViewAccessibilityTimer];
2670 - (void)hideTextInput {
2671 [
self removeEnableFlutterTextInputViewAccessibilityTimer];
2672 _activeView.accessibilityEnabled = NO;
2673 [_activeView resignFirstResponder];
2674 [_activeView removeFromSuperview];
2675 [_inputHider removeFromSuperview];
2678 - (void)triggerAutofillSave:(BOOL)saveEntries {
2679 [_activeView resignFirstResponder];
2684 [
self cleanUpViewHierarchy:YES clearText:YES delayRemoval:NO];
2685 [_autofillContext removeAllObjects];
2686 [
self changeInputViewsAutofillVisibility:YES];
2688 [_autofillContext removeAllObjects];
2691 [
self cleanUpViewHierarchy:YES clearText:!saveEntries delayRemoval:NO];
2692 [
self addToInputParentViewIfNeeded:_activeView];
2695 - (void)setPlatformViewTextInputClient {
2699 [
self removeEnableFlutterTextInputViewAccessibilityTimer];
2700 _activeView.accessibilityEnabled = NO;
2701 [_activeView removeFromSuperview];
2702 [_inputHider removeFromSuperview];
2705 - (void)setTextInputClient:(
int)client withConfiguration:(NSDictionary*)configuration {
2706 [
self resetAllClientIds];
2709 [
self changeInputViewsAutofillVisibility:NO];
2713 case kFlutterAutofillTypeNone:
2714 self.activeView = [
self createInputViewWith:configuration];
2716 case kFlutterAutofillTypeRegular:
2719 self.activeView = [
self updateAndShowAutofillViews:nil
2720 focusedField:configuration
2721 isPasswordRelated:NO];
2723 case kFlutterAutofillTypePassword:
2724 self.activeView = [
self updateAndShowAutofillViews:configuration[kAssociatedAutofillFields]
2725 focusedField:configuration
2726 isPasswordRelated:YES];
2729 [_activeView setTextInputClient:client];
2730 [_activeView reloadInputViews];
2742 [
self cleanUpViewHierarchy:NO clearText:YES delayRemoval:YES];
2753 [_autofillContext removeObjectForKey:autofillId];
2756 [newView configureWithDictionary:configuration];
2757 [
self addToInputParentViewIfNeeded:newView];
2761 if (autofillId &&
AutofillTypeOf(field) == kFlutterAutofillTypeNone) {
2762 [_autofillContext removeObjectForKey:autofillId];
2769 focusedField:(NSDictionary*)focusedField
2770 isPasswordRelated:(BOOL)isPassword {
2773 NSAssert(focusedId,
@"autofillId must not be null for the focused field: %@", focusedField);
2778 focused = [
self getOrCreateAutofillableView:focusedField isPasswordAutofill:isPassword];
2779 [_autofillContext removeObjectForKey:focusedId];
2782 for (NSDictionary* field in fields) {
2784 NSAssert(autofillId,
@"autofillId must not be null for field: %@", field);
2786 BOOL hasHints =
AutofillTypeOf(field) != kFlutterAutofillTypeNone;
2787 BOOL isFocused = [focusedId isEqualToString:autofillId];
2790 focused = [
self getOrCreateAutofillableView:field isPasswordAutofill:isPassword];
2795 _autofillContext[autofillId] = isFocused ? focused
2796 : [
self getOrCreateAutofillableView:field
2797 isPasswordAutofill:isPassword];
2800 [_autofillContext removeObjectForKey:autofillId];
2804 NSAssert(focused,
@"The current focused input view must not be nil.");
2814 isPasswordAutofill:(BOOL)needsPasswordAutofill {
2820 inputView = [inputView initWithOwner:self];
2821 [
self addToInputParentViewIfNeeded:inputView];
2824 [inputView configureWithDictionary:field];
2829 - (UIView*)hostView {
2831 NSAssert(host !=
nullptr,
2832 @"The application must have a host view since the keyboard client "
2833 @"must be part of the responder chain to function. The host view controller is %@",
2839 - (NSArray<UIView*>*)textInputViews {
2840 return _inputHider.subviews;
2853 - (void)cleanUpViewHierarchy:(BOOL)includeActiveView
2854 clearText:(BOOL)clearText
2855 delayRemoval:(BOOL)delayRemoval {
2856 for (UIView* view in
self.textInputViews) {
2858 (includeActiveView || view != _activeView)) {
2860 if (_autofillContext[inputView.autofillId] != view) {
2862 [inputView replaceRangeLocal:NSMakeRange(0, inputView.text.length) withText:@""];
2865 [inputView performSelector:@selector(removeFromSuperview) withObject:nil afterDelay:0.1];
2867 [inputView removeFromSuperview];
2876 - (void)changeInputViewsAutofillVisibility:(BOOL)newVisibility {
2877 for (UIView* view in
self.textInputViews) {
2880 inputView.isVisibleToAutofill = newVisibility;
2892 - (void)resetAllClientIds {
2893 for (UIView* view in
self.textInputViews) {
2896 [inputView setTextInputClient:0];
2902 if (![inputView isDescendantOfView:_inputHider]) {
2903 [_inputHider addSubview:inputView];
2912 UIView* parentView =
self.hostView;
2913 if (_inputHider.superview != parentView) {
2914 [parentView addSubview:_inputHider];
2918 - (void)setTextInputEditingState:(NSDictionary*)state {
2919 [_activeView setTextInputState:state];
2922 - (void)clearTextInputClient {
2923 [_activeView setTextInputClient:0];
2924 _activeView.frame = CGRectZero;
2927 - (void)updateConfig:(NSDictionary*)dictionary {
2928 BOOL isSecureTextEntry = [dictionary[kSecureTextEntry] boolValue];
2929 for (UIView* view in
self.textInputViews) {
2936 if (inputView.isSecureTextEntry != isSecureTextEntry) {
2937 inputView.secureTextEntry = isSecureTextEntry;
2938 [inputView reloadInputViews];
2944 #pragma mark UIIndirectScribbleInteractionDelegate
2946 - (BOOL)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
2947 isElementFocused:(UIScribbleElementIdentifier)elementIdentifier
2948 API_AVAILABLE(ios(14.0)) {
2949 return _activeView.scribbleFocusStatus == FlutterScribbleFocusStatusFocused;
2952 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
2953 focusElementIfNeeded:(UIScribbleElementIdentifier)elementIdentifier
2954 referencePoint:(CGPoint)focusReferencePoint
2955 completion:(
void (^)(UIResponder<UITextInput>* focusedInput))completion
2956 API_AVAILABLE(ios(14.0)) {
2957 _activeView.scribbleFocusStatus = FlutterScribbleFocusStatusFocusing;
2958 [_indirectScribbleDelegate flutterTextInputPlugin:self
2959 focusElement:elementIdentifier
2960 atPoint:focusReferencePoint
2961 result:^(id _Nullable result) {
2962 _activeView.scribbleFocusStatus =
2963 FlutterScribbleFocusStatusFocused;
2964 completion(_activeView);
2968 - (BOOL)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
2969 shouldDelayFocusForElement:(UIScribbleElementIdentifier)elementIdentifier
2970 API_AVAILABLE(ios(14.0)) {
2974 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
2975 willBeginWritingInElement:(UIScribbleElementIdentifier)elementIdentifier
2976 API_AVAILABLE(ios(14.0)) {
2979 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
2980 didFinishWritingInElement:(UIScribbleElementIdentifier)elementIdentifier
2981 API_AVAILABLE(ios(14.0)) {
2984 - (CGRect)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
2985 frameForElement:(UIScribbleElementIdentifier)elementIdentifier
2986 API_AVAILABLE(ios(14.0)) {
2987 NSValue* elementValue = [_scribbleElements objectForKey:elementIdentifier];
2988 if (elementValue == nil) {
2991 return [elementValue CGRectValue];
2994 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
2995 requestElementsInRect:(CGRect)rect
2997 (
void (^)(NSArray<UIScribbleElementIdentifier>* elements))completion
2998 API_AVAILABLE(ios(14.0)) {
2999 [_indirectScribbleDelegate
3000 flutterTextInputPlugin:self
3001 requestElementsInRect:rect
3002 result:^(id _Nullable result) {
3003 NSMutableArray<UIScribbleElementIdentifier>* elements =
3004 [[NSMutableArray alloc] init];
3005 if ([result isKindOfClass:[NSArray class]]) {
3006 for (NSArray* elementArray in result) {
3007 [elements addObject:elementArray[0]];
3010 valueWithCGRect:CGRectMake(
3011 [elementArray[1] floatValue],
3012 [elementArray[2] floatValue],
3013 [elementArray[3] floatValue],
3014 [elementArray[4] floatValue])]
3015 forKey:elementArray[0]];
3018 completion(elements);
3022 #pragma mark - Methods related to Scribble support
3026 if (@available(iOS 14.0, *)) {
3028 if (parentView != nil) {
3029 UIIndirectScribbleInteraction* scribbleInteraction = [[UIIndirectScribbleInteraction alloc]
3030 initWithDelegate:(id<UIIndirectScribbleInteractionDelegate>)self];
3031 [parentView addInteraction:scribbleInteraction];
3038 - (void)resetViewResponder {
3039 _viewResponder = nil;
3043 #pragma mark FlutterKeySecondaryResponder
3058 - (id)flutterFirstResponder {
3059 if (
self.isFirstResponder) {
3062 for (UIView* subView in
self.subviews) {
3063 UIView* firstResponder = subView.flutterFirstResponder;
3064 if (firstResponder) {
3065 return firstResponder;