Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding cursor coordinates to TextInput #36979

Closed
wants to merge 22 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,20 @@ export type NativeProps = $ReadOnly<{|
onSelectionChange?: ?DirectEventHandler<
$ReadOnly<{|
target: Int32,
selection: $ReadOnly<{|start: Double, end: Double|}>,
selection: $ReadOnly<{|
start: Double,
end: Double,
cursorPosition: $ReadOnly<{|
start: $ReadOnly<{|
x: Double,
y: Double,
|}>,
end: $ReadOnly<{|
x: Double,
y: Double,
|}>,
|}>,
|}>,
|}>,
>,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,16 @@ export interface TextInputSelectionChangeEventData extends TargetedEvent {
selection: {
start: number;
end: number;
cursorPosition: {
start: {
x: number;
y: number;
};
end: {
x: number;
y: number;
};
};
};
}

Expand Down Expand Up @@ -817,7 +827,28 @@ export interface TextInputProps
* The start and end of the text input's selection. Set start and end to
* the same value to position the cursor.
*/
selection?: {start: number; end?: number | undefined} | undefined;
selection?:
| {
start: number;
end?: number | undefined;
cursorPosition:
| {
start:
| {
x: number;
y: number;
}
| undefined;
end:
| {
x: number;
y: number;
}
| undefined;
}
| undefined;
}
| undefined;

/**
* The highlight (and cursor on ios) color of the text input
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@ export type FocusEvent = TargetEvent;
type Selection = $ReadOnly<{|
start: number,
end: number,
cursorPosition: $ReadOnly<{|
start: $ReadOnly<{|
x: number,
y: number,
|}>,
end: $ReadOnly<{|
x: number,
y: number,
|}>,
|}>,
|}>;

export type SelectionChangeEvent = SyntheticEvent<
Expand Down Expand Up @@ -837,6 +847,16 @@ export type Props = $ReadOnly<{|
selection?: ?$ReadOnly<{|
start: number,
end?: ?number,
cursorPosition: $ReadOnly<{|
start: $ReadOnly<{|
x: number,
y: number,
|}>,
end: $ReadOnly<{|
x: number,
y: number,
|}>,
|}>,
|}>,

/**
Expand Down
52 changes: 50 additions & 2 deletions packages/react-native/Libraries/Components/TextInput/TextInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,14 @@ type TextInputInstance = React.ElementRef<HostComponent<mixed>> & {
+clear: () => void,
+isFocused: () => boolean,
+getNativeRef: () => ?React.ElementRef<HostComponent<mixed>>,
+setSelection: (start: number, end: number) => void,
+setSelection: (
start: number,
end: number,
cursorPosition: {
start: {x: number, y: number},
end: {x: number, y: number},
},
) => void,
};

let AndroidTextInput;
Expand Down Expand Up @@ -79,6 +86,16 @@ export type TextInputEvent = SyntheticEvent<
range: $ReadOnly<{|
start: number,
end: number,
cursorPosition: $ReadOnly<{|
start: $ReadOnly<{|
x: number,
y: number,
|}>,
end: $ReadOnly<{|
x: number,
y: number,
|}>,
|}>,
|}>,
target: number,
text: string,
Expand Down Expand Up @@ -107,6 +124,16 @@ export type FocusEvent = TargetEvent;
type Selection = $ReadOnly<{|
start: number,
end: number,
cursorPosition: $ReadOnly<{|
start: $ReadOnly<{|
x: number,
y: number,
|}>,
end: $ReadOnly<{|
x: number,
y: number,
|}>,
|}>,
|}>;

export type SelectionChangeEvent = SyntheticEvent<
Expand Down Expand Up @@ -798,7 +825,7 @@ export type Props = $ReadOnly<{|
/**
* Callback that is called when the text input selection is changed.
* This will be called with
* `{ nativeEvent: { selection: { start, end } } }`.
* `{ nativeEvent: { selection: { start, end, cursorPosition: {start: {x, y}, end: {x, y}}} } }`.
*/
onSelectionChange?: ?(e: SelectionChangeEvent) => mixed,

Expand Down Expand Up @@ -875,10 +902,21 @@ export type Props = $ReadOnly<{|
/**
* The start and end of the text input's selection. Set start and end to
* the same value to position the cursor.
* cursorPosition specify the location of the cursor
*/
selection?: ?$ReadOnly<{|
start: number,
end?: ?number,
cursorPosition: $ReadOnly<{|
start: $ReadOnly<{|
x: number,
y: number,
|}>,
end: $ReadOnly<{|
x: number,
y: number,
|}>,
|}>,
|}>,

/**
Expand Down Expand Up @@ -1092,6 +1130,16 @@ function InternalTextInput(props: Props): React.Node {
: {
start: propsSelection.start,
end: propsSelection.end ?? propsSelection.start,
cursorPosition: {
start: {
x: propsSelection.cursorPosition.start.x,
y: propsSelection.cursorPosition.start.y,
},
end: {
x: propsSelection.cursorPosition.end.x,
y: propsSelection.cursorPosition.end.y,
},
},
};

const [mostRecentEventCount, setMostRecentEventCount] = useState<number>(0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,13 +184,29 @@ - (RCTTextSelection *)selection
{
id<RCTBackedTextInputViewProtocol> backedTextInputView = self.backedTextInputView;
UITextRange *selectedTextRange = backedTextInputView.selectedTextRange;

CGRect caretRectStart = [backedTextInputView caretRectForPosition:selectedTextRange.start];
CGPoint cursorPositionStart = caretRectStart.origin;
NSValue *startValue = [NSValue valueWithCGPoint:cursorPositionStart];

CGRect caretRectEnd = [backedTextInputView caretRectForPosition:selectedTextRange.end];
CGPoint cursorPositionEnd = caretRectEnd.origin;
NSValue *endValue = [NSValue valueWithCGPoint:cursorPositionEnd];

NSDictionary *cursorPosition = @{
@"start": startValue,
@"end": endValue
};

return [[RCTTextSelection new]
initWithStart:[backedTextInputView offsetFromPosition:backedTextInputView.beginningOfDocument
toPosition:selectedTextRange.start]
end:[backedTextInputView offsetFromPosition:backedTextInputView.beginningOfDocument
toPosition:selectedTextRange.end]];
toPosition:selectedTextRange.end]
cursorPosition:cursorPosition];
}


- (void)setSelection:(RCTTextSelection *)selection
{
if (!selection) {
Expand Down Expand Up @@ -503,20 +519,62 @@ - (void)textInputDidChange
}
}

- (NSString *)formatPositionValue:(CGFloat)positionValue {
if (positionValue < 0) {
return @"0";
}
NSInteger roundedValue = round(positionValue);
NSString *formattedValue = [NSString stringWithFormat:@"%ld", (long)roundedValue];
return formattedValue;
}


- (void)textInputDidChangeSelection
{
if (!_onSelectionChange) {
return;
}

RCTTextSelection *selection = self.selection;
UITextRange *selectedTextRange = self.backedTextInputView.selectedTextRange;
CGPoint selectionOriginStart = [self.backedTextInputView caretRectForPosition:selectedTextRange.start].origin;

_onSelectionChange(@{
@"selection" : @{
@"start" : @(selection.start),
@"end" : @(selection.end),
},
});
CGRect caretRectEnd = [self.backedTextInputView caretRectForPosition:selectedTextRange.end];

CGPoint selectionOriginEnd = caretRectEnd.origin;
CGFloat cursorHeightEnd = caretRectEnd.size.height;
CGFloat cursorWidthEnd = caretRectEnd.size.width;

NSString *formattedStartY = @"0";
NSString *formattedStartX = @"0";
NSString *formattedEndY = @"0";
NSString *formattedEndX = @"0";

if (selection.start == selection.end && selection.start != 0) {
formattedStartY = [self formatPositionValue:selectionOriginStart.y];
formattedStartX = [self formatPositionValue:selectionOriginStart.x];
}

// We add the height/width of the cursor to the position of the caret to get the bottom right position of the end of the selection
formattedEndY = [self formatPositionValue:(selectionOriginEnd.y + cursorHeightEnd)];
formattedEndX = [self formatPositionValue:(selectionOriginEnd.x + cursorWidthEnd)];

_onSelectionChange(@{
@"selection" : @{
@"start" : @(selection.start),
@"end" : @(selection.end),
@"cursorPosition": @{
@"start": @{
@"x": @([formattedStartX doubleValue]),
@"y": @([formattedStartY doubleValue])
},
@"end": @{
@"x": @([formattedEndX doubleValue]),
@"y": @([formattedEndY doubleValue])
}
},
}
});
}

- (void)updateLocalData
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@

@property (nonatomic, assign, readonly) NSInteger start;
@property (nonatomic, assign, readonly) NSInteger end;
@property (nonatomic, strong, readonly) NSDictionary *cursorPosition;

- (instancetype)initWithStart:(NSInteger)start end:(NSInteger)end;
- (instancetype)initWithStart:(NSInteger)start end:(NSInteger)end cursorPosition:(NSDictionary *)cursorPosition;

@end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@

@implementation RCTTextSelection

- (instancetype)initWithStart:(NSInteger)start end:(NSInteger)end
- (instancetype)initWithStart:(NSInteger)start end:(NSInteger)end cursorPosition:(NSDictionary *)cursorPosition
{
if (self = [super init]) {
_start = start;
_end = end;
_cursorPosition = cursorPosition;
}
return self;
}
Expand All @@ -27,7 +28,10 @@ + (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];
NSDictionary *cursorPosition = json[@"cursorPosition"];
return [[RCTTextSelection alloc] initWithStart:start
end:end
cursorPosition:cursorPosition];
}

return nil;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.view.ViewTreeObserver;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.autofill.HintConstants;
Expand Down Expand Up @@ -1237,13 +1238,47 @@ public void onSelectionChanged(int start, int end) {

// Apparently Android might call this with an end value that is less than the start value
// Lets normalize them. See https://github.com/facebook/react-native/issues/18579

Layout layout = mReactEditText.getLayout();
if (layout == null) {
mReactEditText.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
mReactEditText.getViewTreeObserver().removeOnGlobalLayoutListener(this);
onSelectionChanged(start, end);
}
});
return;
}
int realStart = Math.min(start, end);
int realEnd = Math.max(start, end);
int cursorStartPositionX, cursorStartPositionY, cursorEndPositionX, cursorEndPositionY = 0;

int lineStart = layout.getLineForOffset(realStart);
int baselineStart = layout.getLineBaseline(lineStart);
int ascentStart = layout.getLineAscent(lineStart);
cursorStartPositionX = (int) Math.round(PixelUtil.toDIPFromPixel(layout.getPrimaryHorizontal(realStart)));
cursorStartPositionY = (int) Math.round(PixelUtil.toDIPFromPixel(baselineStart + ascentStart));

int lineEnd = layout.getLineForOffset(realEnd);
int baselineEnd = layout.getLineBaseline(lineEnd);
int ascentEnd = layout.getLineAscent(lineEnd);
cursorEndPositionX = (int) Math.round(PixelUtil.toDIPFromPixel(layout.getPrimaryHorizontal(realEnd)));
cursorEndPositionY = (int) Math.round(PixelUtil.toDIPFromPixel(baselineEnd + ascentEnd));

if (mPreviousSelectionStart != realStart || mPreviousSelectionEnd != realEnd) {
mEventDispatcher.dispatchEvent(
new ReactTextInputSelectionEvent(
mSurfaceId, mReactEditText.getId(), realStart, realEnd));
new ReactTextInputSelectionEvent(
mSurfaceId,
mReactEditText.getId(),
realStart,
realEnd,
cursorStartPositionX,
cursorStartPositionY,
cursorEndPositionX,
cursorEndPositionY
)
);

mPreviousSelectionStart = realStart;
mPreviousSelectionEnd = realEnd;
Expand Down
Loading