From eddebe6bc7497d191f85ff75cfb29d526693c2bc Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 21 Apr 2023 09:44:35 +0200 Subject: [PATCH] enhance onSelectionChange in TextInput with cursorPositionX/Y --- .../Text/TextInput/RCTBaseTextInputView.m | 5 +++- .../Text/TextInput/RCTTextSelection.h | 3 ++- .../Text/TextInput/RCTTextSelection.m | 11 ++++++-- .../textinput/ReactTextInputManager.java | 26 ++++++++++++++++++- .../ReactTextInputSelectionEvent.java | 22 +++++++++++++--- 5 files changed, 59 insertions(+), 8 deletions(-) diff --git a/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.m b/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.m index 0bc14b42f7c944..ec473bcef5df4f 100644 --- a/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.m +++ b/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.m @@ -184,7 +184,8 @@ - (RCTTextSelection *)selection initWithStart:[backedTextInputView offsetFromPosition:backedTextInputView.beginningOfDocument toPosition:selectedTextRange.start] end:[backedTextInputView offsetFromPosition:backedTextInputView.beginningOfDocument - toPosition:selectedTextRange.end]]; + toPosition:selectedTextRange.end] + cursorPosition:[backedTextInputView caretRectForPosition:selectedTextRange.start].origin]; } - (void)setSelection:(RCTTextSelection *)selection @@ -519,6 +520,8 @@ - (void)textInputDidChangeSelection @"selection" : @{ @"start" : @(selection.start), @"end" : @(selection.end), + @"positionY": @(selectionOrigin.y), + @"positionX": @(selectionOrigin.x), }, }); } diff --git a/packages/react-native/Libraries/Text/TextInput/RCTTextSelection.h b/packages/react-native/Libraries/Text/TextInput/RCTTextSelection.h index baa2e0e46a1a23..30970290401922 100644 --- a/packages/react-native/Libraries/Text/TextInput/RCTTextSelection.h +++ b/packages/react-native/Libraries/Text/TextInput/RCTTextSelection.h @@ -14,8 +14,9 @@ @property (nonatomic, assign, readonly) NSInteger start; @property (nonatomic, assign, readonly) NSInteger end; +@property (nonatomic, assign, readonly) CGPoint cursorPosition; -- (instancetype)initWithStart:(NSInteger)start end:(NSInteger)end; +- (instancetype)initWithStart:(NSInteger)start end:(NSInteger)end cursorPosition:(CGPoint)cursorPosition; @end diff --git a/packages/react-native/Libraries/Text/TextInput/RCTTextSelection.m b/packages/react-native/Libraries/Text/TextInput/RCTTextSelection.m index bbcf63bf0bccd1..915438d5bbcdbc 100644 --- a/packages/react-native/Libraries/Text/TextInput/RCTTextSelection.m +++ b/packages/react-native/Libraries/Text/TextInput/RCTTextSelection.m @@ -9,11 +9,12 @@ @implementation RCTTextSelection -- (instancetype)initWithStart:(NSInteger)start end:(NSInteger)end +- (instancetype)initWithStart:(NSInteger)start end:(NSInteger)end cursorPosition:(CGPoint)cursorPosition { if (self = [super init]) { _start = start; _end = end; + _cursorPosition = cursorPosition; } return self; } @@ -27,7 +28,13 @@ + (RCTTextSelection *)RCTTextSelection:(id)json if ([json isKindOfClass:[NSDictionary class]]) { NSInteger start = [self NSInteger:json[@"start"]]; NSInteger end = [self NSInteger:json[@"end"]]; - return [[RCTTextSelection alloc] initWithStart:start end:end]; + CGPoint cursorPosition = CGPointMake( + [self CGFloat:json[@"cursorPositionX"]], + [self CGFloat:json[@"cursorPositionY"]] + ); + return [[RCTTextSelection alloc] initWithStart:start + end:end + cursorPosition:cursorPosition]; } return nil; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java index b9a11bf3ff5d8e..8eea894d7cae98 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java @@ -81,6 +81,7 @@ import java.util.LinkedList; import java.util.Locale; import java.util.Map; +import android.view.ViewTreeObserver; /** Manages instances of TextInput. */ @ReactModule(name = ReactTextInputManager.REACT_CLASS) @@ -1240,6 +1241,24 @@ public ReactSelectionWatcher(ReactEditText editText) { @Override public void onSelectionChanged(int start, int end) { + // Calculate cursor position + Layout layout = mReactEditText.getLayout(); + if (mReactEditText.getLayout() == null) { + mReactEditText.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + mReactEditText.getViewTreeObserver().removeOnGlobalLayoutListener(this); + onSelectionChanged(start, end); + } + }); + return; + } + + int line = layout.getLineForOffset(start); + int baseline = layout.getLineBaseline(line); + int ascent = layout.getLineAscent(line); + float cursorPositionX = layout.getPrimaryHorizontal(start); + float cursorPositionY = baseline + ascent; // Android will call us back for both the SELECTION_START span and SELECTION_END span in text // To prevent double calling back into js we cache the result of the previous call and only // forward it on if we have new values @@ -1252,7 +1271,12 @@ public void onSelectionChanged(int start, int end) { if (mPreviousSelectionStart != realStart || mPreviousSelectionEnd != realEnd) { mEventDispatcher.dispatchEvent( new ReactTextInputSelectionEvent( - mSurfaceId, mReactEditText.getId(), realStart, realEnd)); + mSurfaceId, + mReactEditText.getId(), + realStart, + realEnd, + Math.round(PixelUtil.toDIPFromPixel(cursorPositionX)), + Math.round(PixelUtil.toDIPFromPixel(cursorPositionY)))); mPreviousSelectionStart = realStart; mPreviousSelectionEnd = realEnd; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputSelectionEvent.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputSelectionEvent.java index 161a577f89a32f..94d48d8cd8b17d 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputSelectionEvent.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputSelectionEvent.java @@ -19,17 +19,31 @@ private int mSelectionStart; private int mSelectionEnd; + private float mCursorPositionX; + private float mCursorPositionY; @Deprecated - public ReactTextInputSelectionEvent(int viewId, int selectionStart, int selectionEnd) { - this(-1, viewId, selectionStart, selectionEnd); + public ReactTextInputSelectionEvent( + int viewId, + int selectionStart, + int selectionEnd, + float cursorPositionX, + float cursorPositionY) { + this(-1, viewId, selectionStart, selectionEnd, cursorPositionX, cursorPositionY); } public ReactTextInputSelectionEvent( - int surfaceId, int viewId, int selectionStart, int selectionEnd) { + int surfaceId, + int viewId, + int selectionStart, + int selectionEnd, + float cursorPositionX, + float cursorPositionY) { super(surfaceId, viewId); mSelectionStart = selectionStart; mSelectionEnd = selectionEnd; + mCursorPositionX = cursorPositionX; + mCursorPositionY = cursorPositionY; } @Override @@ -45,6 +59,8 @@ protected WritableMap getEventData() { WritableMap selectionData = Arguments.createMap(); selectionData.putInt("end", mSelectionEnd); selectionData.putInt("start", mSelectionStart); + selectionData.putDouble("cursorPositionX", mCursorPositionX); + selectionData.putDouble("cursorPositionY", mCursorPositionY); eventData.putMap("selection", selectionData); return eventData;