Skip to content

WidgetSpan wrong caret position #107432

Open

Description

@tapeo

Steps to Reproduce

Type the word "widget" inside the text field.

Expected results:
The caret reflects the correct position.

Actual results:
The caret is moved in a wrong position (probably the x - size of the widget span)

Code sample:
import 'package:flutter/material.dart';

void main() {
  runApp(const InputExample());
}

class InputExample extends StatefulWidget {
  const InputExample({Key? key}) : super(key: key);

  @override
  State<InputExample> createState() => _InputExampleState();
}

class _InputExampleState extends State<InputExample> {

  late final WidgetSpanTextFieldController titleController;

  @override
  void initState() {
    titleController = WidgetSpanTextFieldController();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Input Example'),
        ),
        body: Container(
          padding: const EdgeInsets.all(16),
          child: _title(context),
        ),
      ),
    );
  }

  Widget _title(BuildContext context) {
    return TextField(
      controller: titleController,
      textCapitalization: TextCapitalization.none,
      maxLines: null,
      keyboardType: TextInputType.multiline,
      decoration: const InputDecoration(hintText: "Type here"),
      cursorWidth: 5,
      cursorColor: Colors.orange,
      style: const TextStyle(
        color: Colors.black,
        fontSize: 20,
        fontWeight: FontWeight.w500,
      ),
    );
  }
}

class WidgetSpanTextFieldController extends TextEditingController {
  WidgetSpanTextFieldController();

  @override
  TextSpan buildTextSpan({
    required BuildContext context,
    TextStyle? style,
    required bool withComposing,
  }) {
    TextRange? matchedRange;

    if (text.contains("widget")) {
      matchedRange = _findMatchedRange(text);
    }

    if (matchedRange != null) {
      return TextSpan(
        children: [
          _textSpan(matchedRange.textBefore(text)),
          _widgetSpan(matchedRange.textInside(text)),
          _textSpan(matchedRange.textAfter(text)),
        ],
        style: style,
      );
    }

    return TextSpan(text: text, style: style);
  }

  TextRange _findMatchedRange(String text) {
    final RegExp matchPattern = RegExp('widget', caseSensitive: false);
    late TextRange matchedRange;

    for (final Match match in matchPattern.allMatches(text)) {
      matchedRange = TextRange(start: match.start, end: match.end);
    }

    return matchedRange;
  }

  WidgetSpan _widgetSpan(String text) {
    return WidgetSpan(
      alignment: PlaceholderAlignment.middle,
      child: Container(
          height: 20,
          padding: const EdgeInsets.symmetric(horizontal: 5),
          decoration: BoxDecoration(
            color: Colors.redAccent,
            borderRadius: BorderRadius.circular(3),
          ),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text(
                text,
                style: const TextStyle(
                  color: Colors.white,
                  fontWeight: FontWeight.w500,
                  fontSize: 17,
                ),
              ),
            ],
          )),
    );
  }

  TextSpan _textSpan(String text) {
    return TextSpan(text: text);
  }
}

Tested on master and stable channels.

Video:

example.mov

Activity

  1. added
    in triagePresently being triaged by the triage team
    on Jul 12, 2022
  2. huycozy commented on Jul 12, 2022

    @huycozy
    Member

    Hi @tapeo, thanks for filing the issue.
    This issue is reproducible on the latest stable and master channels with provided sample code.
    The caret position displayed wrong after WidgetSpan appears.

    Demo
    ios.mov
    web.mov
    flutter doctor -v
    [✓] Flutter (Channel stable, 3.0.4, on macOS 12.2.1 21D62 darwin-x64, locale en-VN)
        • Flutter version 3.0.4 at /Users/huynq/Documents/GitHub/flutter
        • Upstream repository https://github.com/flutter/flutter.git
        • Framework revision 85684f9300 (3 days ago), 2022-06-30 13:22:47 -0700
        • Engine revision 6ba2af10bb
        • Dart version 2.17.5
        • DevTools version 2.12.2
    
    [✓] Android toolchain - develop for Android devices (Android SDK version 31.0.0)
        • Android SDK at /Users/huynq/Library/Android/sdk
        • Platform android-33, build-tools 31.0.0
        • ANDROID_HOME = /Users/huynq/Library/Android/sdk
        • Java binary at: /Applications/Android Studio.app/Contents/jre/Contents/Home/bin/java
        • Java version OpenJDK Runtime Environment (build 11.0.12+0-b1504.28-7817840)
        • All Android licenses accepted.
    
    [✓] Xcode - develop for iOS and macOS (Xcode 13.3)
        • Xcode at /Applications/Xcode.app/Contents/Developer
        • CocoaPods version 1.11.3
    
    [✓] Chrome - develop for the web
        • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
    
    [!] Android Studio
        • Android Studio at /Applications/Android Studio Preview.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
        ✗ Unable to find bundled Java version.
        • Try updating or re-installing Android Studio.
    
    [✓] Android Studio (version 2021.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 11.0.12+0-b1504.28-7817840)
    
    [✓] IntelliJ IDEA Community Edition (version 2020.3.3)
        • IntelliJ at /Applications/IntelliJ IDEA CE.app
        • 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
    
    [✓] IntelliJ IDEA Community Edition (version 2022.1.1)
        • IntelliJ at /Users/huynq/Library/Application Support/JetBrains/Toolbox/apps/IDEA-C/ch-0/221.5591.52/IntelliJ IDEA CE.app
        • 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
    
    [✓] VS Code (version 1.68.1)
        • VS Code at /Applications/Visual Studio Code.app/Contents
        • Flutter extension version 3.42.0
    
    [✓] Connected device (2 available)
        • macOS (desktop) • macos  • darwin-x64     • macOS 12.2.1 21D62 darwin-x64
        • Chrome (web)    • chrome • web-javascript • Google Chrome 103.0.5060.53
    
    [✓] HTTP Host Availability
        • All required HTTP hosts are available
    
    ! Doctor found issues in 1 category.
    [✓] Flutter (Channel master, 3.1.0-0.0.pre.1609, on macOS 12.2.1 21D62 darwin-x64, locale en-VN)
        • Flutter version 3.1.0-0.0.pre.1609 on channel master at /Users/huynq/Documents/GitHub/flutter_master
        • Upstream repository https://github.com/flutter/flutter.git
        • Framework revision 881b27e836 (35 minutes ago), 2022-07-11 19:23:05 -0700
        • Engine revision abe7b2ccc3
        • Dart version 2.18.0 (build 2.18.0-271.0.dev)
        • DevTools version 2.15.0
    
    [✓] Android toolchain - develop for Android devices (Android SDK version 31.0.0)
        • Android SDK at /Users/huynq/Library/Android/sdk
        • Platform android-33, build-tools 31.0.0
        • ANDROID_HOME = /Users/huynq/Library/Android/sdk
        • Java binary at: /Applications/Android Studio.app/Contents/jre/Contents/Home/bin/java
        • Java version OpenJDK Runtime Environment (build 11.0.12+0-b1504.28-7817840)
        • All Android licenses accepted.
    
    [✓] Xcode - develop for iOS and macOS (Xcode 13.3)
        • Xcode at /Applications/Xcode.app/Contents/Developer
        • Build 13E113
        • CocoaPods version 1.11.3
    
    [✓] Chrome - develop for the web
        • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
    
    [✓] Android Studio (version 2021.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 11.0.12+0-b1504.28-7817840)
    
    [✓] IntelliJ IDEA Community Edition (version 2020.3.3)
        • IntelliJ at /Applications/IntelliJ IDEA CE.app
        • 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
    
    [✓] IntelliJ IDEA Community Edition (version 2022.1.1)
        • IntelliJ at /Users/huynq/Library/Application Support/JetBrains/Toolbox/apps/IDEA-C/ch-0/221.5591.52/IntelliJ IDEA CE.app
        • 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
    
    [✓] VS Code (version 1.68.1)
        • VS Code at /Applications/Visual Studio Code.app/Contents
        • Flutter extension version 3.44.0
    
    [✓] Connected device (3 available)
        • SM T225 (mobile) • R9JT3004VRJ • android-arm64  • Android 11 (API 30)
        • macOS (desktop)  • macos       • darwin-x64     • macOS 12.2.1 21D62 darwin-x64
        • Chrome (web)     • chrome      • web-javascript • Google Chrome 103.0.5060.114
    
    [✓] HTTP Host Availability
        • All required HTTP hosts are available
    
    • No issues found!
  3. added
    a: text inputEntering text in a text field or keyboard related problems
    frameworkflutter/packages/flutter repository. See also f: labels.
    p: material_uimaterial_ui package in flutter/packages
    has reproducible stepsThe issue has been confirmed reproducible and is ready to work on
    a: typographyText rendering, possibly libtxt
    and removed
    in triagePresently being triaged by the triage team
    on Jul 12, 2022
  4. jason-simmons commented on Jul 15, 2022

    @jason-simmons
    Member

    The entire WidgetSpan is treated as a single character by the Flutter engine's text APIs.

    In the example app, the TextEditingController value contains the 6-character string "widget". But when the EditableText calls the text positioning APIs exposed by the TextSpan with the embedded WidgetSpan, those APIs think that the corresponding region only consumes one position in the text. This mismatch will cause problems for the text editor.

    A while ago there was an attempt to build a generalized API for this use case but it looks like it never landed (#80185)

    For now, one potential way to implement this would be overriding the value setter in WidgetSpanTextFieldController to replace the "widget" substring in the text with a single placeholder character. The offsets in the value's TextSelection would also need to be adjusted.

    buildTextSpan can then substitute the placeholder character with a WidgetSpan. The one-to-one mapping between the placeholder's position in the TextEditingValue.text and the WidgetSpan's position in the TextSpan should allow the text editor to function as intended.

  5. kulny commented on Aug 16, 2022

    @kulny

    The entire WidgetSpan is treated as a single character by the Flutter engine's text APIs.

    In the example app, the TextEditingController value contains the 6-character string "widget". But when the EditableText calls the text positioning APIs exposed by the TextSpan with the embedded WidgetSpan, those APIs think that the corresponding region only consumes one position in the text. This mismatch will cause problems for the text editor.

    A while ago there was an attempt to build a generalized API for this use case but it looks like it never landed (#80185)

    For now, one potential way to implement this would be overriding the value setter in WidgetSpanTextFieldController to replace the "widget" substring in the text with a single placeholder character. The offsets in the value's TextSelection would also need to be adjusted.

    buildTextSpan can then substitute the placeholder character with a WidgetSpan. The one-to-one mapping between the placeholder's position in the TextEditingValue.text and the WidgetSpan's position in the TextSpan should allow the text editor to function as intended.

    Is there a way someone could implement this for an arbitrary number of unique WidgetSpans? For example, say you have a list of names that may grow from user input. What would some ideal candidates for the single placeholder text value, and could there be a way to generate unique placeholders for each unique name?

    I originally thought you meant extending PlaceholderSpan to replace the substring, then to replace that extended PlaceholderSpan with the intended WidgetSpan. But that's really just confusing the steps. Could you use unicode values as the unique characters? Or is there some way to create some sort of text value replacing object that can carry data which allows creating the intended WidgetSpan?

    In the case of unicode (forgive my lack of knowledge) it seems that they would not be able to serve as a proper replacement for a single value of text. So what would be a good example of a single placeholder character that is not likely (or impossible) to be inputted by a user?

    Alternatively, would extending WidgetSpan somehow allow changing what the text editing API's expect the position to be to instead be a range?

  6. kulny commented on Aug 16, 2022

    @kulny

    It looks like a (crude) way to work around this is to simply add several WidgetSpans that take up no space until the expected number of characters is matched. I'm not sure how this might affect performance, though.

    @override
    TextSpan buildTextSpan({
        required BuildContext context,
        TextStyle? style,
        bool? withComposing,
      }) {
       TextRange? matchedRange;
    if (text.contains('fly')) {
      print('true');
      matchedRange = _findMatchedRange(text);
    }
    if (matchedRange != null) {
      return TextSpan(
        children: [
          TextSpan(text: matchedRange.textBefore(text)),
          const WidgetSpan(child: FlutterLogo()),
          WidgetSpan(child: SizedBox(height: .1, width: .1)),
          WidgetSpan(child: SizedBox(height: .1, width: .1)),
          TextSpan(text: matchedRange.textAfter(text)),
        ],
        style: style,
      );
    }
    return TextSpan(text: text, style: style);

    EDIT: Added sizes to the SizedBox so that when a user clicks on the TextField where the WidgetSpan is placed it will place the cursor at the end of the string which is replaced by WidgetSpan rather than after the first character.

  7. 13 remaining items

  8. removed
    ⏳Bot is counting down the days until it unassigns the issue
    on Nov 27, 2023
  9. added
    P3Issues that are less important to the Flutter project
    triaged-designTriaged by Design Languages team
    on Jan 4, 2024
  10. nithsua commented on Feb 28, 2024

    @nithsua

    Any update on the issue?

  11. nithsua commented on Feb 29, 2024

    @nithsua

    @tapeo Were you able to find a workaround for this

  12. nithsua commented on Mar 4, 2024

    @nithsua

    Hey @davie1989,
    Were you able to handle changing cursor position and backspaces

  13. davie1989 commented on Mar 4, 2024

    @davie1989

    Hey @davie1989, Were you able to handle the changing cursor position and backspaces

    Hello @nithsua ,

    Yes. I implemented it in a little different way and it work for me.

  14. lucasjinreal commented on Mar 4, 2024

    @lucasjinreal
  15. davie1989 commented on Mar 4, 2024

    @davie1989

    I can send my code after I close from work today. Maybe it would help.

  16. nithsua commented on Mar 4, 2024

    @nithsua

    That would be helpful

  17. nithsua commented on Mar 5, 2024

    @nithsua

    @davie1989 Hey mate, just checking in. Will you be able to share it?

  18. lucas-goldner commented on Feb 18, 2025

    @lucas-goldner
    Contributor

    I have this issue as well, but for some reason it only happens on a certain device (iPhone 15 Pro Max). I haven't figured out, why it only happens on that device... I copied all of their keyboard settings to see if it was related to that, but nothing changed...

  19. added
    packageflutter/packages repository. See also p: labels.
    and removed
    frameworkflutter/packages/flutter repository. See also f: labels.
    on Aug 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3Issues that are less important to the Flutter projecta: text inputEntering text in a text field or keyboard related problemsa: typographyText rendering, possibly libtxtfound in release: 3.0Found to occur in 3.0found in release: 3.1Found to occur in 3.1has reproducible stepsThe issue has been confirmed reproducible and is ready to work onp: material_uimaterial_ui package in flutter/packagespackageflutter/packages repository. See also p: labels.team-designOwned by Design Languages teamtriaged-designTriaged by Design Languages team

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions