Repository navigation
Fix TextField issues in RTL #160516
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
Fix TextField issues in RTL #160516
Changes from all commits
be73115
be86c12
3708881
80e2795
ededfb2
ae4d5b8
7761a02
e528803
7955044
b74362a
5b77c65
50f9c16
5848483
33b0641
aa149d5
d8d5ecb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1567,8 +1567,14 @@ class _SelectableFragment | |
| result = _handleSelectParagraph(selectParagraph.globalPosition); | ||
| } | ||
| case SelectionEventType.granularlyExtendSelection: | ||
| if (event is HorizontalGranularlyExtendSelectionEvent) { | ||
| event.textDirection = paragraph.textDirection; | ||
| } | ||
|
|
||
| final GranularlyExtendSelectionEvent granularlyExtendSelection = | ||
| event as GranularlyExtendSelectionEvent; | ||
| event is HorizontalGranularlyExtendSelectionEvent | ||
| ? event.withTextDirection(paragraph.textDirection) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are we sure that this is the first place we can get the text direction from?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SelectionResult dispatchSelectionEventToChild(Selectable selectable, SelectionEvent event) {
switch (event.type) {
case SelectionEventType.startEdgeUpdate:
didReceiveSelectionEventFor(selectable: selectable, forEnd: false);
ensureChildUpdated(selectable);
case SelectionEventType.endEdgeUpdate:
didReceiveSelectionEventFor(selectable: selectable, forEnd: true);
ensureChildUpdated(selectable);
case SelectionEventType.clear:
clearInternalSelectionStateForSelectable(selectable);
case SelectionEventType.selectAll:
case SelectionEventType.selectWord:
case SelectionEventType.selectParagraph:
break;
case SelectionEventType.granularlyExtendSelection:
case SelectionEventType.directionallyExtendSelection:
didReceiveSelectionEventFor(selectable: selectable);
ensureChildUpdated(selectable);
}
return super.dispatchSelectionEventToChild(selectable, event);
}In |
||
| : event as GranularlyExtendSelectionEvent; | ||
| result = _handleGranularlyExtendSelection( | ||
| granularlyExtendSelection.forward, | ||
| granularlyExtendSelection.isEnd, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -368,6 +368,9 @@ class SelectableRegionState extends State<SelectableRegion> | |
| granularity: TextGranularity.character, | ||
| ), | ||
| ), | ||
| HorizontalExtendSelectionByCharacterIntent: _makeOverridable( | ||
| _GranularlyHorizontalExtendCaretSelectionAction(this, granularity: TextGranularity.character), | ||
| ), | ||
| ExtendSelectionToNextWordBoundaryIntent: _makeOverridable( | ||
| _GranularlyExtendCaretSelectionAction<ExtendSelectionToNextWordBoundaryIntent>( | ||
| this, | ||
|
|
@@ -1640,6 +1643,23 @@ class SelectableRegionState extends State<SelectableRegion> | |
| _finalizeSelectableRegionStatus(); | ||
| } | ||
|
|
||
| void _granularlyHorizontalExtendSelection(TextGranularity granularity, bool right) { | ||
| _directionalHorizontalBaseline = null; | ||
| if (!_selectionDelegate.value.hasSelection) { | ||
| return; | ||
| } | ||
| _selectable?.dispatchSelectionEvent( | ||
| HorizontalGranularlyExtendSelectionEvent( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So we don't know the text direction at this point because this is the whole SelectableRegionState, which internally could have different TextDirection widgets, such as with my example app (in #160516 (review)). |
||
| right: right, | ||
| isEnd: _determineIsAdjustingSelectionEnd(right), | ||
| granularity: granularity, | ||
| ), | ||
| ); | ||
| _updateSelectedContentIfNeeded(); | ||
| _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing; | ||
| _finalizeSelectableRegionStatus(); | ||
| } | ||
|
|
||
| double? _directionalHorizontalBaseline; | ||
|
|
||
| void _directionallyExtendSelection(bool forward) { | ||
|
|
@@ -2027,6 +2047,20 @@ class _GranularlyExtendCaretSelectionAction<T extends DirectionalCaretMovementIn | |
| } | ||
| } | ||
|
|
||
| class _GranularlyHorizontalExtendCaretSelectionAction | ||
| extends _GranularlyExtendCaretSelectionAction<HorizontalExtendSelectionByCharacterIntent> { | ||
| _GranularlyHorizontalExtendCaretSelectionAction(super.state, {required super.granularity}); | ||
|
|
||
| @override | ||
| void invokeAction(HorizontalExtendSelectionByCharacterIntent intent, [BuildContext? context]) { | ||
| if (intent.collapseSelection) { | ||
| // Selectable region never collapses selection. | ||
| return; | ||
| } | ||
| state._granularlyHorizontalExtendSelection(granularity, intent.right); | ||
| } | ||
| } | ||
|
|
||
| class _DirectionallyExtendCaretSelectionAction<T extends DirectionalCaretMovementIntent> | ||
| extends _NonOverrideAction<T> { | ||
| _DirectionallyExtendCaretSelectionAction(this.state); | ||
|
|
@@ -3032,6 +3066,14 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai | |
| } | ||
| int targetIndex = event.isEnd ? currentSelectionEndIndex : currentSelectionStartIndex; | ||
| SelectionResult result = dispatchSelectionEventToChild(selectables[targetIndex], event); | ||
|
|
||
| if (event is HorizontalGranularlyExtendSelectionEvent) { | ||
| // In HorizontalGranularlyExtendSelectionEvent, the "forward" property may not be valid | ||
| // until the TextDirection is determined. Therefore, we recreate the event with the | ||
| // proper TextDirection to ensure it reflects the correct directionality for the selection extension. | ||
| event = event.withTextDirection(event.textDirection); | ||
|
Comment on lines
+3071
to
+3074
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The idea that we have events that are not fully accurate until being updated like this seems kind of error-prone. I'm still thinking through what the better way might be though.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One possible solution could be to handle horizontal events directly in widgets that need to support them, such as Text, instead of translating |
||
| } | ||
|
|
||
| if (event.forward) { | ||
| assert(result != SelectionResult.previous); | ||
| while (targetIndex < selectables.length - 1 && result == SelectionResult.next) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -115,6 +115,38 @@ class ExtendSelectionByCharacterIntent extends DirectionalCaretMovementIntent { | |
| : super(forward, collapseSelection); | ||
| } | ||
|
|
||
| /// Extends, or moves the current selection from the current | ||
| /// [TextSelection.extent] position to the left or the right character | ||
| /// boundary. | ||
| class HorizontalExtendSelectionByCharacterIntent extends ExtendSelectionByCharacterIntent { | ||
| /// Creates an [HorizontalExtendSelectionByCharacterIntent]. | ||
| const HorizontalExtendSelectionByCharacterIntent({ | ||
| required this.right, | ||
| required super.collapseSelection, | ||
| TextDirection textDirection = TextDirection.ltr, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The creator of the intent is likely going to have no information about who the receiving text field is going to be, so how does it know what text direction to use?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, this field is assigned a value once the direction is specified. Check this.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I think that's a bit problematic as the widget that emits the intent may not know who the receiving text field is going to be (as a result it has not way to determine the current writing direction since different text fields can have different writing directions). IOW, when you're constructing the intent, there's no way to determine the correct Also the intent should be (and it is) immutable. |
||
| }) : super(forward: textDirection == TextDirection.rtl ? !right : right); | ||
|
|
||
| /// Whether the input field, if applicable, should perform the text editing | ||
| /// operation from the current caret location towards the right direction. | ||
| /// | ||
| /// Unless otherwise specified by the recipient of this intent, this parameter | ||
| /// uses the visual order of characters on the screen to determine the | ||
| /// direction, and is not affected by the logical order of the text. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: let's make it simpler. Maybe just whether the receiving text field should move the caret to the left / right by one character?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I should mention that there are many shortcuts for the TextField.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry I wasn't clear, I meant maybe the documentation can be a bit simpler. |
||
| final bool right; | ||
|
|
||
| /// Creates a new instance of [HorizontalExtendSelectionByCharacterIntent] with the | ||
| /// specified [textDirection]. | ||
| /// | ||
| /// The new instance retains the current values of [right] and [collapseSelection], | ||
| /// allowing dynamic adjustment of the intent based on the given text direction. | ||
| HorizontalExtendSelectionByCharacterIntent withTextDirection(TextDirection textDirection) => | ||
| HorizontalExtendSelectionByCharacterIntent( | ||
| right: right, | ||
| textDirection: textDirection, | ||
| collapseSelection: collapseSelection, | ||
| ); | ||
| } | ||
|
|
||
| /// Extends, or moves the current selection from the current | ||
| /// [TextSelection.extent] position to the previous or the next word | ||
| /// boundary. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you explain this change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When
textDirection: TextDirection.rtlis set for a TextField, pressing the down arrow key in the TextField triggers the following line first:Let’s analyze this function:
When the
TextFieldis in LTR mode, the conditionoffset == Offset.zerois true. However, when it is in RTL mode, the condition will not hold true (see the attached image).As a result,
rawMetricsis returned with modifications. The returned output is equivalent in value torawMetrics.Therefore, I replaced
identicalwithlistEquals, but I believe the correct solution is to fix the issue in thecomputeLineMetricsfunction itself.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That makes sense to me. The
identicalis incorrect for RTL.I wonder if the _TextPainterLayoutCacheWithOffset.lineMetrics getter should do the shifting and then assign the shifted line metrics to its _cachedLinesMetrics. That's a risky change though, so it should probably be done in a separate PR.
I think that @MrMontazer you should continue with your current approach, but create an issue with this information. Point to your
listEqualsand say that should be changed toidenticalalong with the refactor I described above, for performance reasons.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current implementation of
VerticalCaretMovementRunuses this hack to detect whether TextPainter recomputed its layout. So the use ofidenticalhere is intended.IIRC the
offsethere has nothing to do with the writing direction of the text.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These changes are related to #139201.
I agree that this is incorrect. I think I will revert it to the previous state and leave fixing this issue for another pull request