Repository: flutter
Issue: Caret can fail to move on keyboard left/right arrow in Indic script
Labels: a: text input,framework,a: internationalization,a: desktop,P2,team-text-input,triaged-text-input

This is a milder symptom in the same repro recipe as at #118403. In that issue, as you try to move left or right through some text by using the arrow keys, the cursor/caret sometimes jumps in the wrong direction. This turns out to be related to #50563 and #98516, and I'll be sending a PR soon (→ #122480) that fixes it.

After that fix #122480, as you try to move left or right using the arrow keys, the cursor never jumps back in the wrong direction, but it will sometimes be "sticky": you hit the arrow key and the cursor stays where it was. When you hit the same arrow key a second time, then the cursor moves where it should. (This symptom is already present before #122480, but doesn't stick out in the presence of the more severe issue #118403.)

From some debugging, the root of the issue seems to be that `string.characters` for #118403's example string "प्राप्त वर्णन प्रव्रुति" breaks up the string more finely than SkParagraph does. Using `string.characters` produces the list `['प्', 'रा', 'प्', 'त', ' ', 'व', 'र्', 'ण', 'न', ' ', 'प्', 'र', 'व्', 'रु', 'ति']`, so that for example "प्रा" and "प्त" are each two characters. And then it appears that that gives the sequence of positions that the `EditableText` tries to move the cursor through.

If you paste the same string into a browser and move through it with arrow keys, the sequence of pieces you find is instead `['प्रा', 'प्त', ' ', 'व', 'र्ण', 'न', ' ', 'प्र', 'व्रु', 'ति']`. (At least that's what I find in Chrome on Linux.) And when `TextPainter._getRectFromDownstream` calls `Paragraph.getBoxesForRange`, the results suggest that SkParagraph agrees with that. So when you're at the start and hit the right-arrow key, we try to move the cursor to the position between "प्" and "रा"; SkParagraph considers that the middle of the character "प्रा", and we round off to painting the cursor back at the start of that character.

This example string is in Devanagari script. I'm guessing the issue affects other Indic scripts, which compose graphemes in similar ways.

## Steps to Reproduce

1. Run the provided code sample and click e.g. on "Bengali"

**Expected results:** The caret moves each time you hit a left/right arrow key. (Unless you're trying to move left from the start, or right from the end.)

**Actual results:** When you hit the right arrow key from the start of the text, the caret does not move. When you hit it a second time, then the caret moves.

<details>
<summary>Code sample</summary>

```dart
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  late final TextEditingController _controller;
  TextSelection _selection = const TextSelection.collapsed(offset: 0);

  @override
  void initState() {
    super.initState();
    _controller = TextEditingController();
    _controller.addListener(() {
      setState(() {
        _selection = _controller.selection;
      });
    });
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            TextField(
              controller: _controller,
              style: const TextStyle(fontSize: 28.0),
            ),
            const SizedBox(height: 20.0),
            Text('Selection: $_selection'),
            const SizedBox(height: 50.0),
            Wrap(
              children: [
                TextButton(
                  onPressed: () => _controller.text = 'Wszyscy ludzie',
                  child: const Text('Latin'),
                ),
                TextButton(
                  onPressed: () => _controller.text = '(بالإنجليزية: United Nations)',
                  child: const Text('BiDi Latin-Arabic'),
                ),
                TextButton(
                  onPressed: () => _controller.text = 'يمتساوين',
                  child: const Text('Arabic'),
                ),
                TextButton(
                  onPressed: () => _controller.text = 'تمام افراد بشر',
                  child: const Text('Farsi'),
                ),
                TextButton(
                  onPressed: () => _controller.text = 'כל בני אדם נולדו בני.',
                  child: const Text('Hebrew'),
                ),
                TextButton(
                  onPressed: () => _controller.text = 'सभी मनुष्यों को गौरव',
                  child: const Text('Devanagari'),
                ),
                TextButton(
                  onPressed: () => _controller.text = 'മനുഷ്യരെല്ലാവരും',
                  child: const Text('Malayalam'),
                ),
                TextButton(
                  onPressed: () => _controller.text = 'རྒྱུད་ཡོངས་ལ་སྐྱེ',
                  child: const Text('Tibetan'),
                ),
                TextButton(
                  onPressed: () => _controller.text = 'பிறக்கின்றனர்',
                  child: const Text('Tamil'),
                ),
                TextButton(
                  onPressed: () => _controller.text = 'স্বাধীনভাবে',
                  child: const Text('Bengali'),
                ),
                TextButton(
                  onPressed: () => _controller.text = 'มนุษย์ทั้งหลายเ',
                  child: const Text('Thai'),
                ),
                TextButton(
                  onPressed: () => _controller.text = 'ក្នុងផ្នែកសេចក្ដីថ្លៃថ្នូរនិងសិទ្ធិ។',
                  child: const Text('Khmer'),
                ),
                TextButton(
                  onPressed: () => _controller.text = 'ဂုဏ်သိက္ခာဖြင့်',
                  child: const Text('Myanmar'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}
```

</details>