flutter / flutter

Flutter makes it easy and fast to build beautiful apps for mobile and beyond
https://flutter.dev
BSD 3-Clause "New" or "Revised" License
164.8k stars 27.16k forks source link

getLocalRectForCaret Returns inconsistent results based on what the last key press was #135354

Open Cellaryllis opened 11 months ago

Cellaryllis commented 11 months ago

Is there an existing issue for this?

Steps to reproduce

Expected results

The red line follows where the caret currently is

Actual results

The red line is positioned at the top of the text box when typing, however on backspace the caret goes to the correct location in the text box and highlights the text.

Code sample

Code sample ```dart import 'package:crashtest/test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_portal/flutter_portal.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', home: MyHomePage(title: 'Flutter Demo Home Page'), ); } } class MyHomePage extends StatefulWidget { MyHomePage({Key? key, required this.title}) : super(key: key); final String title; @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State { GlobalKey editableText = GlobalKey(); TextEditingController controller = TextEditingController(text: "Hello world!"); FocusNode focus = FocusNode(); @override void initState() { super.initState(); controller.addListener(() { setState(() {}); }); } Widget getfollower() { if (editableText.currentState == null) { return SizedBox.shrink(); } EditableTextState state = editableText.currentState!; Rect rect = state.renderEditable.getLocalRectForCaret(controller.selection.base); print("rect: " + rect.toString()); return IgnorePointer( ignoring: true, child: Padding( padding: EdgeInsets.only(top: rect.top), child: SizedBox( height: 20, width: MediaQuery.of(context).size.width, child: Container( child: SizedBox.expand(), color: Colors.red.withOpacity(.2))))); } @override Widget build(BuildContext context) { return Portal( child: Scaffold( appBar: AppBar( title: Text( "Text getLocalRectForCaret issue", )), body: Center( child: SingleChildScrollView( child: Column( children: [ //Other widgets Container( margin: const EdgeInsets.symmetric(horizontal: 20), color: Color.fromARGB(255, 48, 48, 48), child: Padding( padding: const EdgeInsets.all(15.0), child: PortalTarget( anchor: Aligned( follower: Alignment.topLeft, target: Alignment.topLeft, widthFactor: 1, offset: Offset(0, 0), backup: const Aligned( follower: Alignment.bottomLeft, target: Alignment.topLeft, widthFactor: 1, ), ), portalFollower: getfollower(), child: EditableText( key: editableText, controller: controller, focusNode: focus, minLines: null, maxLines: null, cursorColor: Colors.blue, backgroundCursorColor: Colors.red, style: TextStyle(), ))), ), ], ), ), ), )); } } ```

Screenshots or Video

Screenshots / Video demonstration https://github.com/flutter/flutter/assets/95936504/274a0eef-6923-498a-93c3-8dff4c071ed7

Logs

Logs When just typing any text on the second text field line ```console rect: Rect.fromLTRB(-0.1, 0.2, 1.9, 16.2) ``` When backspacing on the second text field line ```console Rect.fromLTRB(75.5, 16.2, 77.5, 32.2) ```

Flutter Doctor output

Doctor output ```console Doctor summary (to see all details, run flutter doctor -v): [√] Flutter (Channel stable, 3.10.6, on Microsoft Windows [Version 10.0.19045.3448], locale en-US) [√] Windows Version (Installed version of Windows is version 10 or higher) [√] Android toolchain - develop for Android devices (Android SDK version 33.0.2) [X] Chrome - develop for the web (Cannot find Chrome executable at .\Google\Chrome\Application\chrome.exe) ! Cannot find Chrome. Try setting CHROME_EXECUTABLE to a Chrome executable. [√] Visual Studio - develop for Windows (Visual Studio Community 2022 17.5.3) [√] Android Studio (version 2022.1) [√] VS Code, 64-bit edition (version 1.76.2) [√] Connected device (3 available) [√] Network resources ```
Cellaryllis commented 11 months ago

For completeness sake and for confirming that the package is indeed not the issue: Simply move the getLocalRectForCaret into the listener and print its result, and delete all portal related code and simply have the text field in the container.

Cellaryllis commented 11 months ago

What is interesting is that in the RenderEditable, the _lastCaretRect has the correct offset value when breakpointing, but that value is inaccessible in code and so I can't use it as a workaround.

Cellaryllis commented 11 months ago

Furthermore for testing if this is dev specific:

dam-ease commented 11 months ago

Hi @Cellaryllis. Thanks for filing this. I can reproduce this on the latest master and stable channels following the reproduction steps above. Although I see different behaviours on Android and iOS.

Android iOS

Additionally, when the text input is done in between the initial text set, this seems to work as expected on both Android and iOS.

Android iOS

Additional Note from triage:

Code Sample Used

```dart import 'package:flutter/material.dart'; import 'package:flutter_portal/flutter_portal.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', home: MyHomePage(title: 'Flutter Demo Home Page'), ); } } class MyHomePage extends StatefulWidget { MyHomePage({Key? key, required this.title}) : super(key: key); final String title; @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State { GlobalKey editableText = GlobalKey(); TextEditingController controller = TextEditingController(text: "Hello world!"); FocusNode focus = FocusNode(); @override void initState() { super.initState(); controller.addListener(() { setState(() {}); }); } Widget getfollower() { if (editableText.currentState == null) { return SizedBox.shrink(); } EditableTextState state = editableText.currentState!; Rect rect = state.renderEditable.getLocalRectForCaret(controller.selection.base); print("rect: " + rect.toString()); return IgnorePointer( ignoring: true, child: Padding( padding: EdgeInsets.only(top: rect.top), child: SizedBox( height: 20, width: MediaQuery.of(context).size.width, child: Container( child: SizedBox.expand(), color: Colors.red.withOpacity(.2))))); } @override Widget build(BuildContext context) { return Portal( child: Scaffold( appBar: AppBar( title: Text( "Text getLocalRectForCaret issue", )), body: Center( child: SingleChildScrollView( child: Column( children: [ //Other widgets Container( margin: const EdgeInsets.symmetric(horizontal: 20), color: Color.fromARGB(255, 48, 48, 48), child: Padding( padding: const EdgeInsets.all(15.0), child: PortalTarget( anchor: Aligned( follower: Alignment.topLeft, target: Alignment.topLeft, widthFactor: 1, offset: Offset(0, 0), backup: const Aligned( follower: Alignment.bottomLeft, target: Alignment.topLeft, widthFactor: 1, ), ), portalFollower: getfollower(), child: EditableText( key: editableText, controller: controller, focusNode: focus, minLines: null, maxLines: null, cursorColor: Colors.blue, backgroundCursorColor: Colors.red, style: TextStyle(), ))), ), ], ), ), ), )); } } ```

stable, master flutter doctor -v

``` [✓] Flutter (Channel stable, 3.13.5, on macOS 13.0 22A380 darwin-arm64, locale en-NG) • Flutter version 3.13.5 on channel stable at /Users/damilolaalimi/sdks/flutter • Upstream repository https://github.com/flutter/flutter.git • Framework revision 12fccda598 (6 days ago), 2023-09-19 13:56:11 -0700 • Engine revision bd986c5ed2 • Dart version 3.1.2 • DevTools version 2.25.0 [✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0) • Android SDK at /Users/damilolaalimi/Library/Android/sdk • Platform android-34, build-tools 34.0.0 • ANDROID_HOME = /Users/damilolaalimi/Library/Android/sdk • Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java • Java version OpenJDK Runtime Environment (build 17.0.6+0-17.0.6b802.4-9586694) • All Android licenses accepted. [✓] Xcode - develop for iOS and macOS (Xcode 14.3.1) • Xcode at /Applications/Xcode.app/Contents/Developer • Build 14E300c • CocoaPods version 1.12.1 [✓] Chrome - develop for the web • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome [✓] Android Studio (version 2022.2) • Android Studio at /Applications/Android Studio.app/Contents • Flutter plugin can be installed from: 🔨 https://plugins.jetbrains.com/plugin/9212-flutter • Dart plugin can be installed from: 🔨 https://plugins.jetbrains.com/plugin/6351-dart • Java version OpenJDK Runtime Environment (build 17.0.6+0-17.0.6b802.4-9586694) [✓] VS Code (version 1.82.2) • VS Code at /Applications/Visual Studio Code.app/Contents • Flutter extension version 3.50.0 [✓] Connected device (4 available) • sdk gphone64 arm64 (mobile) • emulator-5554 • android-arm64 • Android 14 (API 34) (emulator) • iPhone 14 Pro Max (mobile) • BB55E997-7F31-462D-B3B1-6B177D9B40C7 • ios • com.apple.CoreSimulator.SimRuntime.iOS-16-4 (simulator) • macOS (desktop) • macos • darwin-arm64 • macOS 13.0 22A380 darwin-arm64 • Chrome (web) • chrome • web-javascript • Google Chrome 116.0.5845.187 [✓] Network resources • All expected network resources are available. • No issues found! ``` ``` [✓] Flutter (Channel master, 3.15.0-7.0.pre.6, on macOS 13.0 22A380 darwin-arm64, locale en-NG) • Flutter version 3.15.0-7.0.pre.6 on channel master at /Users/damilolaalimi/fvm/versions/master • Upstream repository https://github.com/flutter/flutter.git • Framework revision d5a8cd3840 (3 hours ago), 2023-09-25 03:12:22 -0400 • Engine revision e1c1022c2d • Dart version 3.2.0 (build 3.2.0-191.0.dev) • DevTools version 2.28.0-dev.12 [✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0) • Android SDK at /Users/damilolaalimi/Library/Android/sdk • Platform android-34, build-tools 34.0.0 • ANDROID_HOME = /Users/damilolaalimi/Library/Android/sdk • Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java • Java version OpenJDK Runtime Environment (build 17.0.6+0-17.0.6b802.4-9586694) • All Android licenses accepted. [✓] Xcode - develop for iOS and macOS (Xcode 14.3.1) • Xcode at /Applications/Xcode.app/Contents/Developer • Build 14E300c • CocoaPods version 1.12.1 [✓] Chrome - develop for the web • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome [✓] Android Studio (version 2022.2) • Android Studio at /Applications/Android Studio.app/Contents • Flutter plugin can be installed from: 🔨 https://plugins.jetbrains.com/plugin/9212-flutter • Dart plugin can be installed from: 🔨 https://plugins.jetbrains.com/plugin/6351-dart • Java version OpenJDK Runtime Environment (build 17.0.6+0-17.0.6b802.4-9586694) [✓] VS Code (version 1.82.2) • VS Code at /Applications/Visual Studio Code.app/Contents • Flutter extension version 3.50.0 [✓] Connected device (4 available) • sdk gphone64 arm64 (mobile) • emulator-5554 • android-arm64 • Android 14 (API 34) (emulator) • iPhone 14 Pro Max (mobile) • BB55E997-7F31-462D-B3B1-6B177D9B40C7 • ios • com.apple.CoreSimulator.SimRuntime.iOS-16-4 (simulator) • macOS (desktop) • macos • darwin-arm64 • macOS 13.0 22A380 darwin-arm64 • Chrome (web) • chrome • web-javascript • Google Chrome 116.0.5845.187 [✓] Network resources • All expected network resources are available. • No issues found! ```

Cellaryllis commented 11 months ago

@dam-ease apologies for the late reply. Thanks for the confirmation it's an issue in the framework. Is there any workaround I might be able to do to target the correct location on the text input field?

maRci002 commented 5 months ago

The problem is that the TextController's listener is called before the layout has occurred, so the underlying TextPainter's text still holds the old text.

I can think of these workarounds:

This code updates the render object's text ```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 const MaterialApp( debugShowCheckedModeBanner: false, home: MyHomePage(), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({super.key}); @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State { final _textFieldKey = GlobalKey>(); final _textFieldController = TextEditingController(); @override void initState() { super.initState(); _textFieldController.addListener(showCaretPosition); } Future showCaretPosition() async { final editableTextKey = (_textFieldKey.currentState! as TextSelectionGestureDetectorBuilderDelegate).editableTextKey; final editableTextState = editableTextKey.currentState!; final editableTextRender = editableTextState.renderEditable; // update renderer final nextText = editableTextState.buildTextSpan(); editableTextRender.text = nextText; final cursorTextPosition = _textFieldController.selection.base; final caretRect = editableTextRender.getLocalRectForCaret(cursorTextPosition); final editableGlobalPosition = editableTextRender.localToGlobal(Offset.zero); final overlayState = Overlay.of(context); final overlay = OverlayEntry( builder: (context) { return Positioned( left: editableGlobalPosition.dx + caretRect.left + 2.0, top: editableGlobalPosition.dy + caretRect.top, child: const Material( color: Colors.blueGrey, child: Text( 'Caret', style: TextStyle(fontSize: 20), ), ), ); }, ); overlayState.insert(overlay); await Future.delayed(const Duration(milliseconds: 500)); overlay.remove(); } @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Padding( padding: const EdgeInsets.all(40), child: TextField( key: _textFieldKey, controller: _textFieldController, style: const TextStyle(fontSize: 20), minLines: 1, maxLines: 2, ), ), ), ); } } ```

https://github.com/flutter/flutter/assets/8436039/89901d38-5549-470a-8e57-016d82a305a3