Skip to content

Add WidgetSpan support for TextField/RenderEditable #30688

@znshje

Description

@znshje

I don't need a passage editor like Zefyr which has styles like BOLD and ITALIC, what I want is a text field in which I can insert plain text, inline image(faces/emoji), block image, mention symbol(@) and topic symbol(#topic#).
It can be implemented on Android by SpannableString. I hope I can do it on Flutter, too.
So anyone knows how to do this?

Activity

added
a: text inputEntering text in a text field or keyboard related problems
f: material designflutter/packages/flutter/material repository.
frameworkflutter/packages/flutter repository. See also f: labels.
on Jun 21, 2019
HansMuller

HansMuller commented on Jun 21, 2019

@HansMuller
Contributor
GaryQian

GaryQian commented on Jun 27, 2019

@GaryQian
Contributor

Text.rich supports building text from a tree of InlineSpans, which in turn support any of the properties in TextStyle. We have recently added support for inline widgets. Simply include a WidgetSpan in your InlineSpan tree. This includes images, as well as any other arbitrary widget. You can add a @ symbol and make it linkable by adding a GestureRecognizer in a TextSpan.

Please refer to our (master channel) docs for more info.

janheinrichmerker

janheinrichmerker commented on Jul 12, 2019

@janheinrichmerker

@GaryQian I don't think you've fully understood the issue.
There is Text.rich() which allows for formatting a TextWidget, but there is no TextField which could be styled like that.

GaryQian

GaryQian commented on Jul 12, 2019

@GaryQian
Contributor

Ahh I see, my bad! I don't think there is a built in way right now, but the building blocks are there, it is a matter of assembling them such that we allow inputting custom InlineSpan trees into a text field.

Sp4Rx

Sp4Rx commented on Jul 24, 2019

@Sp4Rx

Could you elaborate about the implementation? @GaryQian

GaryQian

GaryQian commented on Jul 24, 2019

@GaryQian
Contributor

So what kind of functionality would you require for this "Rich TextField"? We currently have the InlineSpan tree API that allows you to build a tree of InlineSpan objects that inherit TextStyles.

Would the functionality of being able to append independently styled InlineSpans to the contents of the TextField be sufficient?

Sp4Rx

Sp4Rx commented on Jul 25, 2019

@Sp4Rx

As I can see from InlineSpan example that it can have multiple TextField with different styles. But I want a single TextField where I can insert different styles just like RichText. For example take Facebook's or any major platform's @mention. In java I have done this with SpannableString like @nullptrjzz mentioned.

GaryQian

GaryQian commented on Jul 26, 2019

@GaryQian
Contributor

Well since we don't have this explicit capability yet, it would be helpful if you could provide pseudo/fake sample code on how you would like the API to work.

There are a couple of ways this could work, one of them being keeping a stack of TextStyles and push/pop methods. Any text inserted through keyboard or through a manual method would then have the style the top TextStyle in the stack. This system would be very similar to our existing ParagraphBuilder works.

Other approaches could be either more or less structured, but it would be very helpful to have examples of the kind of code you want to be able to write to achieve what you want. Feel free to make up methods/classes/API! This way, we can add the functionality that would best fit your (and other's uses)

Sp4Rx

Sp4Rx commented on Jul 27, 2019

@Sp4Rx

Flutter has a class called Chip. I don't know it is the right question to ask or not, is it possible to insert Chip in TextField?

As flutter don't provide inserting styling by positions like SpannableString so I can think of the pseudo code similar to RichText

//pseudo code
RichTextField(
  style: DefaultTextStyle.of(context).style,
  children: <SubTextField>[
    SubTextField(
      style: TextStyle(
        color: Colors.white,
      ),
    ),
    SubTextField(),
  ],
)

67 remaining items

GaryQian

GaryQian commented on Jul 20, 2021

@GaryQian
Contributor

@jasonhoo95 This particular issue is actually more difficult to solve depending on what you want. Currently, the background color is drawn as part of the text layout and painting, while the selection highlight is drawn separately. We are presented with two options: Draw highlight behind the text (including the bg) or over the text (including bg). For most cases, drawing behind is correct as we want the text to remain readable. However, in this case, the bg just ends up obscuring the highlight.

You could try modifying RenderEditable to paint the highlight after the text and playing with the opacity, but it would still obscure the text. Slipping the highlight between the text and the bg is a fairly complex change that modifies the layout engine and I don't think there is enough demand for that capability to justify it as of now.

jasonhoo95

jasonhoo95 commented on Jul 22, 2021

@jasonhoo95

@GaryQian Hmm but can I ask, in ios native layout engine is possible to set the highlight to overlay above the bg and text am I right? Oo man this is bad, i was really hope to develop an app with rich textfield with flutter, cause flutter it is really easy to use, looks like now is not going to happen, but in the future will u able to fix it with highlight overlay on top bg?

derkro99

derkro99 commented on Aug 23, 2021

@derkro99

Hello @spideythewebhead, I have also rewrote the buildTextSpan method, but as soon as I add a WidgetSpan into the mix:

The following assertion was thrown during performLayout():
Assertion failed:
..\…\widgets\widget_span.dart:105
dimensions != null
is not true


The relevant error-causing widget was
TextField-[GlobalKey#d0c03 rectoinputText]
lib\…\flashCardCreate_tile\flashcardcreate_tile.dart:1972
When the exception was thrown, this was the stack
C:/b/s/w/ir/cache/builder/src/out/host_debug/dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 236:49  throw_
C:/b/s/w/ir/cache/builder/src/out/host_debug/dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 29:3    assertFailed
packages/flutter/src/widgets/widget_span.dart 105:22                                                                       build
packages/flutter/src/painting/text_span.dart 252:14                                                                        build
packages/flutter/src/painting/text_painter.dart 569:7                                                                      layout
...
The following RenderObject was being processed when the exception was fired: RenderEditable#a1504 relayoutBoundary=up73 NEEDS-LAYOUT NEEDS-PAINT NEEDS-COMPOSITING-BITS-UPDATE
RenderObject: RenderEditable#a1504 relayoutBoundary=up73 NEEDS-LAYOUT NEEDS-PAINT NEEDS-COMPOSITING-BITS-UPDATE
    parentData: <none> (can use size)
    constraints: BoxConstraints(w=1335.8, 0.0<=h<=Infinity)
    size: MISSING
    cursorColor: Color(0x00f46b45)
    showCursor: ValueNotifier<bool>#47e2f(true)
    maxLines: null
    minLines: 1
    selectionColor: Color(0x662196f3)
    textScaleFactor: 1.0
    locale: en_US
    selection: TextSelection(baseOffset: -1, extentOffset: -1, affinity: TextAffinity.downstream, isDirectional: false)
    offset: ScrollPositionWithSingleContext#4e4f3(offset: 0.0, range: null..null, viewport: null, ScrollableState, ClampingScrollPhysics -> RangeMaintainingScrollPhysics, IdleScrollActivity#e2324, ScrollDirection.idle)
    text: TextSpan
        debugLabel: (englishLike subhead 2014).merge((blackRedmond subtitle1).apply)
        inherit: false
        color: Color(0xdd000000)
        family: Noto
        size: 16.0
        weight: 400
        baseline: alphabetic
Assertion failed:
..\…\rendering\box.dart:1930
hasSize
"RenderBox was not laid out: RenderSemanticsAnnotations#17089 relayoutBoundary=up69 NEEDS-PAINT NEEDS-COMPOSITING-BITS-UPDATE"

The relevant error-causing widget was
TextField-[GlobalKey#d0c03 rectoinputText]
lib\…\flashCardCreate_tile\flashcardcreate_tile.dart:1972

@Paul-cbt Did you manage to find a solution for this problem?
Edit: It turns out i was using the wrong channel. Using master channel "fixes" this

justin-m-lacy

justin-m-lacy commented on Sep 1, 2021

@justin-m-lacy

Hello! In what version is this api available? I am currently using 2.2.1, i was reading through TextEditingController
and i saw this method TextSpan buildTextSpan({required BuildContext context, TextStyle? style , required bool withComposing}) {..}, in the comments it says we should override this if we want to provide customized text.

I wrote this function

  @override
  TextSpan buildTextSpan({required BuildContext context, TextStyle? style, required bool withComposing}) {
    final atIndex = text.indexOf('@');
    var spans = <InlineSpan>[];

    if (atIndex != -1) {
      spans.add(TextSpan(text: text.substring(0, atIndex)));
      spans.add(
        WidgetSpan(
          alignment: PlaceholderAlignment.middle,
          child: Card(
            child: Padding(
              padding: const EdgeInsets.all(4.0),
              child: Text('@'),
            ),
          ),
        ),
      );
      spans.add(TextSpan(text: text.substring(1 + atIndex)));
    } else {
      spans.add(TextSpan(text: text));
    }

    return TextSpan(
      children: spans,
    );
  }

What is does, it replaces the @ symbol with a widget, but this crashes with this assertion
'package:flutter/src/widgets/widget_span.dart': Failed assertion: line 105 pos 12: 'dimensions != null': is not true.

I probably dont understand how it should be done, is there any guide or its not available yet? thanks!

I seem to be having this same problem - and most likely for the same reason. I'm using custom widgets for tagged elements in input text but WidgetSpan appears to break TextField because of the dimensions issue.

Arrowsome

Arrowsome commented on Oct 15, 2021

@Arrowsome

Any update on this issue?
I Wanted to draw a simple rounded background color behind my text and it seems possible using WidgetSpan (or if Paint class supported rounded corners in fill style, which doesn't).
It's 2021 and I can't implement an effect from 2014 in Flutter!

wrteam-priyansh

wrteam-priyansh commented on Nov 17, 2021

@wrteam-priyansh

I'm trying to implement it by myself and got something "kind of working".

This is the piece of code I put inside the "buildTextSpan" method of TextEditingController : Capture d’écran 2021-03-20 à 14 48 21

And what it looks like in action :

WidgetSpan_Offset_Problem.mov
As you can see there's an offset problem of two characters because of the two WidgetSpans added before the TextSpan. There's also the problem of the cursor not adapting its height to the WidgetSpan. I just did what @GaryQian said above and took a look at RenderParagraph and Copy-Pasted some code inside RenderEditable (as well as some minor modifications inside EditableText). I'll try and read those file and see if I can solve those problems.

I'm also building a RichTextField using the default TextField Widget Flutter is offering and it looks kind of promising, here's a peek :

RichTextField_Peek.mov
You'll be able to customize the toolbar or provide your own and create as many styles as you want.

Hello @Teio07, can you please share some initial building blocks to build RichTextField with some basic functionality?
Thanks

hwasoocho

hwasoocho commented on Feb 13, 2022

@hwasoocho

Any progress on this issue?

Renzo-Olivares

Renzo-Olivares commented on Mar 17, 2022

@Renzo-Olivares
Contributor

I made a simple example to show that WidgetSpans render inside of a TextField inline with other text. Core support for this was added in #83537 . Is this what this issue is asking for? If not please explain further.

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final WidgetSpanTextEditingController _controller =
      WidgetSpanTextEditingController(
          text: 'The quick brown fox jumps over the lazy \uffff dog.');

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: TextField(
          controller: _controller,
          maxLines: null,
        ),
      ),
    );
  }
}

class WidgetSpanTextEditingController extends TextEditingController {
  WidgetSpanTextEditingController({String? text})
      : super.fromValue(text == null
            ? TextEditingValue.empty
            : TextEditingValue(text: text));

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

    TextRange? matchedRange;

    if (text.contains('\uffff')) {
      matchedRange = _findMatchedRange(text);
    }

    if (matchedRange != null) {
      return TextSpan(
        children: [
          TextSpan(text: matchedRange.textBefore(text)),
          const WidgetSpan(child: FlutterLogo()),
          TextSpan(text: matchedRange.textAfter(text)),
        ],
        style: style,
      );
    }

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

  TextRange _findMatchedRange(String text) {
    final RegExp matchPattern = RegExp(RegExp.escape('\uffff'));
    late TextRange matchedRange;
    
    for (final Match match in matchPattern.allMatches(text)) {
      matchedRange = TextRange(start: match.start, end: match.end);
    }

    return matchedRange;
  }
}

Screen Shot 2022-03-17 at 1 33 53 PM

added
waiting for customer responseThe Flutter team cannot make further progress on this issue until the original reporter responds
on Mar 17, 2022
yohom

yohom commented on Mar 18, 2022

@yohom

I made a simple example to show that WidgetSpans render inside of a TextField inline with other text. Core support for this was added in #83537 . Is this what this issue is asking for? If not please explain further.

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final WidgetSpanTextEditingController _controller =
      WidgetSpanTextEditingController(
          text: 'The quick brown fox jumps over the lazy \uffff dog.');

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: TextField(
          controller: _controller,
          maxLines: null,
        ),
      ),
    );
  }
}

class WidgetSpanTextEditingController extends TextEditingController {
  WidgetSpanTextEditingController({String? text})
      : super.fromValue(text == null
            ? TextEditingValue.empty
            : TextEditingValue(text: text));

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

    TextRange? matchedRange;

    if (text.contains('\uffff')) {
      matchedRange = _findMatchedRange(text);
    }

    if (matchedRange != null) {
      return TextSpan(
        children: [
          TextSpan(text: matchedRange.textBefore(text)),
          const WidgetSpan(child: FlutterLogo()),
          TextSpan(text: matchedRange.textAfter(text)),
        ],
        style: style,
      );
    }

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

  TextRange _findMatchedRange(String text) {
    final RegExp matchPattern = RegExp(RegExp.escape('\uffff'));
    late TextRange matchedRange;
    
    for (final Match match in matchPattern.allMatches(text)) {
      matchedRange = TextRange(start: match.start, end: match.end);
    }

    return matchedRange;
  }
}
Screen Shot 2022-03-17 at 1 33 53 PM

@Renzo-Olivares
The buildTextSpan renders WidgetSpan successfully in a TextField, but the cursor can not locate to the last position. When you try to move the cursor to the last position, the cursor jumps to the first position.
My flutter doctor:

Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 2.10.3, on macOS 12.2.1 21D62 darwin-arm, locale en-CN)
[✓] Android toolchain - develop for Android devices (Android SDK version 31.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 13.3)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2021.1)
[✓] IntelliJ IDEA Ultimate Edition (version 2021.3.2)
[✓] IntelliJ IDEA Community Edition (version 2021.2.4)
[✓] IntelliJ IDEA Ultimate Edition (version 2021.2.4)
[✓] VS Code (version 1.65.2)
Screenrecorder-2022-03-18-09-00-36-63.mp4
Renzo-Olivares

Renzo-Olivares commented on Mar 18, 2022

@Renzo-Olivares
Contributor

@yohom thanks for trying it out. I was able to reproduce your issue on stable as well. It is fixed on the master branch (video below).

screen-20220318-130824.mp4

I'll close this for now since core support for a WidgetSpan in a TextField/RenderEditable is there. If there are more specific issues with WidgetSpan inside of a TextField I encourage anyone to open a more targeted issue.

github-actions

github-actions commented on Apr 1, 2022

@github-actions

This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.

locked as resolved and limited conversation to collaborators on Apr 1, 2022
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

    P1High-priority issues at the top of the work lista: text inputEntering text in a text field or keyboard related problemsc: new featureNothing broken; request for a new capabilityc: proposalA detailed proposal for a change to Flutterf: material designflutter/packages/flutter/material repository.frameworkflutter/packages/flutter repository. See also f: labels.waiting for customer responseThe Flutter team cannot make further progress on this issue until the original reporter responds

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @seeps001@Hixie@mindeng@Renzo-Olivares@MatrixDev

        Issue actions

          Add WidgetSpan support for TextField/RenderEditable · Issue #30688 · flutter/flutter