Skip to content

Conversation

@tgucio
Copy link
Contributor

@tgucio tgucio commented Nov 10, 2025

This PR implements horizontal character traversal (using arrow keys) in text fields and paragraphs using actual glyph information from text layout, as opposed to "guessing" character boundaries from the underlying text. This allows for correct horizontal caret traversal of RTL text as well as correctly positioning the caret between characters in complex scripts (e.g. Indic) that rely on text shaping.

Before:
before

After:
after

Fixes #34610
Fixes #78660
Maybe fixes #120049

Pre-launch Checklist

  • I read the [Contributor Guide] and followed the process outlined there for submitting PRs.
  • I read the [Tree Hygiene] wiki page, which explains my responsibilities.
  • I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement].
  • I signed the [CLA].
  • I listed at least one issue that this PR fixes in the description above.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making, or this PR is [test-exempt].
  • I followed the [breaking change policy] and added [Data Driven Fixes] where supported.
  • All existing and new tests are passing.

@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 Nov 10, 2025
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a GlyphBoundary to enable correct caret traversal in text with mixed right-to-left and left-to-right scripts, which is a great improvement. The implementation is mostly solid, but the core logic in _moveBeyondGlyphBoundary within editable_text.dart has some critical issues. There are TODO comments indicating failing tests and untested edge cases that must be addressed. The logic for handling LTR/RTL transitions is also very complex, potentially buggy, and has formatting issues that violate the repository's style guide. I've left detailed comments on these points.

Comment on lines 5318 to 5347
// LTR/RTL or RTL/LTR boundary: resort to searching for the next glyph.
if (boundary.isNormalized) {
// LTR->RTL: start skipping at the next boundary.
do {
boundary = nextBoundary;
offset = math.max(0, nextOffset);
nextOffset = forward ? boundary.start : boundary.end - 1;
nextBoundary = textBoundary.getTextBoundaryAt(nextOffset);
} while (nextOffset < _value.text.length
&& nextOffset >= 0
&& nextBoundary.isValid
&& nextBoundary.isNormalized == boundary.isNormalized);
offset = math.max(0, boundary.start);
affinity = forward ? TextAffinity.upstream : TextAffinity.downstream;
} else {
// RTL->LTR: start skipping at the current boundary.
nextBoundary = boundary;
do {
boundary = nextBoundary;
offset = math.max(0, nextOffset);
nextOffset = forward ? boundary.start : boundary.end - 1;
nextBoundary = textBoundary.getTextBoundaryAt(nextOffset);
} while (nextOffset < _value.text.length
&& nextOffset >= 0
&& nextBoundary.isValid
&& nextBoundary.isNormalized == boundary.isNormalized);
offset = math.max(0, nextBoundary.end);
affinity = forward ? TextAffinity.downstream : TextAffinity.upstream;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

high

The logic for handling LTR/RTL boundary transitions is quite complex and hard to follow. For instance, the offset variable is updated inside the do-while loops but this value seems to be unused, as it's immediately overwritten after the loop. This could be a potential source of bugs.

Additionally, the indentation for the lines after the do-while loops (lines 5330-5331 and 5344-5345) seems incorrect. They should be indented to be within their respective if/else blocks. This appears to be a violation of the Flutter Style Guide which requires all code to be formatted with dart format.1

Given the complexity and the existing TODOs about failing tests, this section might benefit from simplification or more extensive comments explaining the state transitions. Could you refactor this to improve clarity and correctness, perhaps by breaking it down into smaller helper methods and fixing the formatting?

Style Guide References

Footnotes

  1. All Dart code is formatted using dart format. This is enforced by CI.

@tgucio tgucio changed the title Initial implementation Use glyph boundaries for horizontal character traversal Nov 11, 2025
@LongCatIsLooong
Copy link
Contributor

LongCatIsLooong commented Nov 25, 2025

Sorry for the delay.

TextPainter is where the caret positioning logic lives so you may want to take look at its getOffsetForCaret implementation instead of doing this in RenderEditable. IIRC, TextPainter currently does not expose information about glyph clusters (the PR seems to be using graphemeClusterRange, that's the text range of the grapheme cluster instead of the glyph cluster, that's actually what TextPainter currently uses to calculate the caret position).

I havent looked into how bidi traversal is implemented in this PR. However with SKParagraph not exposing the bidi regions in the text with the associated bidi levels, I suspect there isn't an easy algorithm that can handle nested bidi regions, by just looking at the writing direction of each grapheme cluster. To implement it correctly I think you would have to implement the unicode bidi algorithm in dart (and unfortunately I don't think we have access to the unicode character properties) to get the bidi regions and then compute the traversal order.

@tgucio
Copy link
Contributor Author

tgucio commented Nov 26, 2025

@LongCatIsLooong the implementation in this PR is actually based on ui.Paragraph.getGlyphInfoAt which exposes glyph direction. The logic to get boundaries is implemented in GlyphBoundary as an extension of TextBoundary to keep the current design in text_painter.dart. The idea is to use actual rendered glyph information instead of relying on the Characters package to "guess" where the glyphs should start/end.

@flutter-dashboard
Copy link

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.

Copy link
Contributor

@justinmc justinmc left a comment

Choose a reason for hiding this comment

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

@tgucio I really appreciate this ambitious PR. I left some quick thoughts but I'm interested in whether @LongCatIsLooong thinks this is feasible or not.

}

TextAffinity affinity = TextAffinity.downstream;
TextRange boundary = textBoundary.getTextBoundaryAt(offset);
Copy link
Contributor

Choose a reason for hiding this comment

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

Does TextBoundary.getTextBoundaryAt possibly require iterating the entire string? I'm thinking about what the performance implications of this PR could be. I assume it will be negligible in most cases, but we should think it through.

Copy link
Contributor

Choose a reason for hiding this comment

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

We should probably include a high level test that actually puts a bidirectional string into an EditableText and traverses it with arrow keys.

this,
_characterBoundary,
_moveBeyondTextBoundary,
_moveBeyondGlyphBoundary,
Copy link
Contributor

Choose a reason for hiding this comment

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

Are there any other places that would benefit from _moveBeyondGlyphBoundary?

Comment on lines +5243 to +5247
TextPosition _moveBeyondGlyphBoundary(
TextPosition extent,
bool forward,
TextBoundary textBoundary,
) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: If you accept _value as a parameter, could this be static? Just thinking it might make this complex method easier to understand if so.

@Renzo-Olivares Renzo-Olivares self-requested a review January 15, 2026 21:59
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.

Projects

None yet

3 participants