Skip to content

Fix TextField issues in RTL - #160516

Closed
MrMontazer wants to merge 16 commits into
flutter:masterfrom
MrMontazer:master
Closed

MrMontazer wants to merge 16 commits into
flutter:masterfrom
MrMontazer:master

Conversation

@MrMontazer

Copy link
Copy Markdown

@flutter-dashboard

Copy link
Copy Markdown

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.

@github-actions github-actions Bot added a: text input Entering text in a text field or keyboard related problems framework flutter/packages/flutter repository. See also f: labels. labels Dec 18, 2024
@github-actions github-actions Bot added engine flutter/engine related. See also e: labels. a: animation Animation APIs p: material_ui material_ui package in flutter/packages a: accessibility Accessibility, e.g. VoiceOver or TalkBack. (aka a11y) f: scrolling Viewports, list views, slivers, etc. p: cupertino_ui cupertino_ui package in flutter/packages d: api docs Issues with https://api.flutter.dev/ d: examples Sample code and demos f: routes Navigator, Router, and related APIs. f: gestures flutter/packages/flutter/gestures repository. a: desktop Running on desktop f: focus Focus traversal, gaining or losing focus and removed engine flutter/engine related. See also e: labels. a: animation Animation APIs a: accessibility Accessibility, e.g. VoiceOver or TalkBack. (aka a11y) f: scrolling Viewports, list views, slivers, etc. p: cupertino_ui cupertino_ui package in flutter/packages d: api docs Issues with https://api.flutter.dev/ d: examples Sample code and demos f: routes Navigator, Router, and related APIs. f: gestures flutter/packages/flutter/gestures repository. a: desktop Running on desktop labels Dec 20, 2024
@MrMontazer

Copy link
Copy Markdown
Author

Are you still planning to update this PR to use a new Intent as mentioned in #160516 (comment)?

Yes, I’m working on it and hope to finish soon.

@MrMontazer
MrMontazer marked this pull request as draft January 20, 2025 21:31
@flutter-dashboard

Copy link
Copy Markdown

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 package:flutter.

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing.

@MrMontazer
MrMontazer marked this pull request as ready for review January 22, 2025 12:45
@MrMontazer

Copy link
Copy Markdown
Author

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 TextDirection for translation and we don’t have sufficient information about TextDirection when creating the intents, this became a significant challenge. I attempted to translate the intents along the path as soon as we had enough information about TextDirection. (Perhaps there’s a better solution to this challenge that I’m unaware of.)

@justinmc justinmc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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)).

Comment on lines +3071 to +3074
// 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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The 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 right to forward. This would likely require significant changes.

final GranularlyExtendSelectionEvent granularlyExtendSelection =
event as GranularlyExtendSelectionEvent;
event is HorizontalGranularlyExtendSelectionEvent
? event.withTextDirection(paragraph.textDirection)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The 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 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.

@MrMontazer

Copy link
Copy Markdown
Author

Two useful resources to better understand RTL:

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.

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 سلام":

  • At the beginning of the video, the cursor is positioned at the start of the text.
  • Initially, the behavior of the text fields is shown using only the right arrow key. Later, the behavior of the same text field during selection is demonstrated.
logical string visual string
ezgif-5-599325e6cb ezgif-5-8d2b422dfc

I tried to implement the behavior of the Logical String.
*In the visual string video I provided as an example, there is an unconventional behavior during selection.

// 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)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.

const HorizontalExtendSelectionByCharacterIntent({
required this.right,
required super.collapseSelection,
TextDirection textDirection = TextDirection.ltr,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The 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.
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.

@LongCatIsLooong LongCatIsLooong Feb 14, 2025 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@dkwingsmt

Copy link
Copy Markdown
Contributor

@MrMontazer Seems like there are some comments left, can you take a look?

@flutter-dashboard

Copy link
Copy Markdown

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 package:flutter.

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing.

@MrMontazer
MrMontazer marked this pull request as draft March 10, 2025 18:49
@Piinks

Piinks commented May 13, 2025

Copy link
Copy Markdown
Contributor

Hey @MrMontazer we have not heard back here, would you like to continue working on this change?

@MrMontazer

Copy link
Copy Markdown
Author

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.

@Piinks

Piinks commented May 14, 2025

Copy link
Copy Markdown
Contributor

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.

@Piinks Piinks closed this May 14, 2025
kishan-dhankecha pushed a commit to kishan-dhankecha/flutter that referenced this pull request Sep 25, 2026
…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/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a: text input Entering text in a text field or keyboard related problems framework flutter/packages/flutter repository. See also f: labels. p: material_ui material_ui package in flutter/packages team-text-input Owned by Text Input team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Arrow keys in RTL move the wrong way TextField with TextAlign.right throws Failed assertion: line 206 pos 12: 'isValid': is not true.

5 participants