Repository navigation
Fix TextField issues in RTL - #160516
Fix TextField issues in RTL#160516MrMontazer wants to merge 16 commits into
Conversation
|
It looks like this pull request may not have tests. Please make sure to add tests before merging. If you need an exemption, contact "@test-exemption-reviewer" in the #hackers channel in Discord (don't just cc them here, they won't see it!). If you are not sure if you need tests, consider this rule of thumb: the purpose of a test is to make sure someone doesn't accidentally revert the fix. Ask yourself, is there anything in your PR that you feel it is important we not accidentally revert back to how it was before your fix? Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. The test exemption team is a small volunteer group, so all reviewers should feel empowered to ask for tests, without delegating that responsibility entirely to the test exemption group. |
Yes, I’m working on it and hope to finish soon. |
|
This pull request has been changed to a draft. The currently pending flutter-gold status will not be able to resolve until a new commit is pushed or the change is marked ready for review again. For more guidance, visit Writing a golden file test for Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. |
|
Although I agreed with creating new intents, the more I worked on this fork, the more I realized there are additional shortcuts that need to behave in reverse in RTL. Since this type of intent requires |
justinmc
left a comment
There was a problem hiding this comment.
This doesn't seem to work in all cases, see the line "I am normal text in a RTL Directionality." in my example app given in #160516 (review). The arrows used to go in the correct direction on master, but now they are reversed.
@Renzo-Olivares Can you review this as well? I'm wondering about the changes to selection events and how they should be aware of the text direction.
It seems like we need to think through the right way to do this, but I think we're moving in the right direction. Thanks for your work following up with this PR @MrMontazer. RTL is a bit of a blind spot for me so I appreciate when we can get help.
| return; | ||
| } | ||
| _selectable?.dispatchSelectionEvent( | ||
| HorizontalGranularlyExtendSelectionEvent( |
There was a problem hiding this comment.
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)).
| // 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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
One possible solution could be to handle horizontal events directly in widgets that need to support them, such as Text, instead of translating right to forward. This would likely require significant changes.
| final GranularlyExtendSelectionEvent granularlyExtendSelection = | ||
| event as GranularlyExtendSelectionEvent; | ||
| event is HorizontalGranularlyExtendSelectionEvent | ||
| ? event.withTextDirection(paragraph.textDirection) |
There was a problem hiding this comment.
Are we sure that this is the first place we can get the text direction from?
There was a problem hiding this comment.
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 selectable_region.dart line 2288, by detecting if selectable.widget.delegate is _SelectableTextContainerDelegate, we might be able to determine the TextDirection earlier.
However, it’s important to note that _SelectableTextContainerDelegate is defined in a different file.
|
Two useful resources to better understand RTL:
English is an LTR language, so when you set LTR text to RTL, you should expect some behaviors to reverse. There are various solutions for handling this (LINK). The video below demonstrates two different behaviors for the text "Hello سلام":
I tried to implement the behavior of the Logical String. |
| // Use the implementation detail of the computeLineMetrics method to figure | ||
| // out if the current text layout has been invalidated. | ||
| if (!identical(newLineMetrics, _lineMetrics)) { | ||
| if (!listEquals(newLineMetrics, _lineMetrics)) { |
There was a problem hiding this comment.
The current implementation of VerticalCaretMovementRun uses this hack to detect whether TextPainter recomputed its layout. So the use of identical here is intended.
IIRC the offset here has nothing to do with the writing direction of the text.
| /// | ||
| /// 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. |
There was a problem hiding this comment.
nit: let's make it simpler. Maybe just whether the receiving text field should move the caret to the left / right by one character?
There was a problem hiding this comment.
I should mention that there are many shortcuts for the TextField.
The simplest shortcuts are the left and right arrow keys.
However, combination keys that include the left or right arrow, such as Shift + Left Arrow, also exhibit reversed behavior in RTL (right-to-left) mode.
Do you still think the same way?
There was a problem hiding this comment.
Sorry I wasn't clear, I meant maybe the documentation can be a bit simpler.
| const HorizontalExtendSelectionByCharacterIntent({ | ||
| required this.right, | ||
| required super.collapseSelection, | ||
| TextDirection textDirection = TextDirection.ltr, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Actually, this field is assigned a value once the direction is specified. Check this.
However, I know that the way this behavior is implemented is not ideal, so your feedback could be helpful in improving the implementation of this issue.
There was a problem hiding this comment.
this field is assigned a value once the direction is specified
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 TextDirection.
Also the intent should be (and it is) immutable.
|
@MrMontazer Seems like there are some comments left, can you take a look? |
|
This pull request executed golden file tests, but it has not been updated in a while (20+ days). Test results from Gold expire after as many days, so this pull request will need to be updated with a fresh commit in order to get results from Gold. For more guidance, visit Writing a golden file test for Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. |
|
Hey @MrMontazer we have not heard back here, would you like to continue working on this change? |
|
Hey @Piinks, I’ve tried various methods in the past, but each had its own issues. So, I don’t think I can come up with a suitable solution for this problem. Also, I currently don’t have enough time to look into it further. Sorry about that. |
No trouble at all! Thank you for your time and effort spent here, it's really appreciated. I am going to close this PR for now then. |
…191223) Fixes flutter#139201 ## The bug `TextPainter.computeLineMetrics` translates the paragraph's line metrics by `paintOffset`, which is only non-zero when the text is not left aligned. That translation built a brand new list on every call: ```dart final List<ui.LineMetrics> rawMetrics = layout.lineMetrics; return offset == Offset.zero ? rawMetrics : rawMetrics.map((ui.LineMetrics metrics) => _shiftLineMetrics(metrics, offset)).toList(growable: false); ``` `VerticalCaretMovementRun.isValid` uses the identity of that list to detect text layout changes: ```dart final List<ui.LineMetrics> newLineMetrics = _editable._textPainter.computeLineMetrics(); // Use the implementation detail of the computeLineMetrics method to figure // out if the current text layout has been invalidated. if (!identical(newLineMetrics, _lineMetrics)) { _isValid = false; } ``` So whenever the paint offset is non-zero, a vertical caret run was invalid the instant it was created. This matches @LongCatIsLooong's diagnosis in flutter#139201 (comment). The user visible symptom is a **single line** `TextField` / `CupertinoTextField` with `TextAlign.center` or `TextAlign.right`: a single line field lays its text out with an unbounded width (`RenderEditable._adjustConstraints` passes `double.infinity` as `maxWidth` when `!_isMultiline`), so `contentWidth != paragraph.width` and the paint offset is non-zero. Pressing the up or down arrow key then trips `assert(isValid)` in `VerticalCaretMovementRun.moveNext` / `movePrevious` in debug mode, and silently leaves the caret where it is in release mode. ## The fix Cache the translated list on `_TextPainterLayoutCacheWithOffset`, keyed on the paint offset it was computed with (`paintOffset` is derived from the mutable `contentWidth`, so it can change without a relayout). `computeLineMetrics` then keeps returning the same list instance for as long as the text layout and the paint offset are unchanged, which restores the identity contract `VerticalCaretMovementRun` depends on. It also stops allocating a fresh list of `LineMetrics` on every call for every non left aligned paragraph. ## Relation to flutter#160516 flutter#160516 fixed this issue by relaxing `isValid` to `listEquals` instead. That works, but it compares every line's metrics on every arrow key press, it keeps allocating the intermediate list, and it makes a run stay valid across a genuine layout change that happens to produce equal metrics (the run's `_positionCache` would then be stale). This PR fixes the identity contract that was broken instead, and leaves `isValid` alone. That PR was closed for reasons unrelated to this issue (the RTL arrow direction work for flutter#78660). ## Tests - `TextPainter line metrics are cached when the paint offset is not zero` — asserts `computeLineMetrics()` returns an identical list across calls, for every `TextAlign`. - `TextPainter line metrics are shifted by the paint offset` — guards the translation itself, which now happens in a new place. - `run in a single line field that is not left aligned` in `editable_text_shortcuts_test.dart` — the end to end regression test; it throws the `'isValid': is not true` assertion without this patch. All three fail on master and pass with this change. `test/painting`, `test/rendering/editable_test.dart`, `test/rendering/paragraph_test.dart`, `test/widgets/editable_text_test.dart`, `test/widgets/editable_text_shortcuts_test.dart`, `test/widgets/text_test.dart`, `test/widgets/selectable_region_test.dart`, `test/material/text_field_test.dart`, `test/material/text_form_field_test.dart`, `test/material/selectable_text_test.dart` and `test/cupertino/text_field_test.dart` are green. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making. - [x] All existing and new tests are passing. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/


TextAlign.rightthrowsFailed assertion: line 206 pos 12: 'isValid': is not true.#139201