custom_text 1.4.3 copy "custom_text: ^1.4.3" to clipboard
custom_text: ^1.4.3 copied to clipboard

Highly customisable text widget and controller to enable styling and gesture actions.

Pub Version Flutter CI codecov

A highly customisable text widget that enables decorations and gesture actions, and a special TextEditingController that makes most of the same functionality available also in a text field.

This package is useful for making partial strings in text (e.g. URLs, email addresses or phone numbers) react to tap, long-press and hover gestures, or for only highlighting particular strings with colors and different font settings. You can configure the appearance and the behaviour using multiple definitions consisting of regular expressions, text styles, gesture handlers, etc.

Usage by example #

Most of the examples here are contained in the sample app in the example folder. Just click on the link below to open its web version and see it in action.

Web Demo

The app shows the source code with keywords highlighted, which itself is made possible by this package (plus the highlight package used as an external parser).

Code highlighting

Simplest example #

simple.dart (Code / Demo)

Image - Simplest example image

A very basic example of how to apply a colour to URLs and email addresses using preset matchers.

Gestures are not available on the coloured strings in this example.

CustomText(
  'URL: https://example.com/\n'
  'Email: foo@example.com',
  definitions: const [
    TextDefinition(matcher: UrlMatcher()),
    TextDefinition(matcher: EmailMatcher()),
  ],
  matchStyle: const TextStyle(color: Colors.lightBlue),
)

Preset matchers

The matchers listed below are for general use. If a stricter pattern is necessary, overwrite the preset pattern or create a custom matcher.

Styles and actions per definition #

styles_and_actions.dart (Code / Demo)

Image - Styles and actions per definition

An example to decorate URLs, email addresses and phone numbers, and also enable them to be tapped and long-pressed.

All the three are styled, but only phone numbers among them are styled differently with the unique matchStyle and tapStyle.

Tip:
To open a browser or another app when a string is tapped or long-pressed, use the url_launcher package or equivalent in the onTap and/or onLongPress handlers.

CustomText(
  'URL: https://example.com/\n'
  'Email: foo@example.com\n'
  'Tel: +1-012-3456-7890',
  definitions: [
    const TextDefinition(matcher: UrlMatcher()),
    const TextDefinition(matcher: EmailMatcher()),
    TextDefinition(
      matcher: const TelMatcher(),
      // Styles and handlers specified in a definition take
      // precedence over the equivalent arguments of CustomText.
      matchStyle: const TextStyle(
        color: Colors.green,
        decoration: TextDecoration.underline,
      ),
      tapStyle: const TextStyle(color: Colors.orange),
      onTap: (details) => print(details.actionText),
      onLongPress: (details) => print('[Long press] ${details.actionText}'),
    ),
  ],
  matchStyle: const TextStyle(
    color: Colors.lightBlue,
    decoration: TextDecoration.underline,
  ),
  tapStyle: const TextStyle(color: Colors.indigo),
  onTap: (details) => print(details.actionText),
  onLongPress: (details) => print('[Long press] ${details.actionText}'),
)

Overwriting a match pattern #

overwriting_pattern.dart (Code / Demo)

Image - Overwriting match pattern

An example to replace the default pattern of TelMatcher.

The new pattern here regards only the {3 digits}-{4 digits}-{4 digits} format as a phone number.

CustomText(
  'Tel: +1-012-3456-7890',
  definitions: const [
    TextDefinition(matcher: TelMatcher(r'\d{3}-\d{4}-\d{4}')),
  ],
  matchStyle: const TextStyle(color: Colors.lightBlue),
  onTap: (details) => print(details.actionText),
)

Custom match pattern #

custom_pattern.dart (Code / Demo)

Image - Custom match pattern

An example to parse hashtags with a custom pattern and apply styles to them.

A hashtag has a wide variety of definitions, but here as an example, it is defined as a string that starts with "#" followed by an alphabet and then alphanumerics, and is enclosed with white spaces.

TextDefinition(
  matcher: PatternMatcher(r'(?<=\s|^)\#[a-zA-Z][a-zA-Z0-9]{1,}(?=\s|$)'),
),

Alternatively, you can define a matcher by extending TextMatcher. This allows you to distinguish the custom matcher from others by its unique type.

class HashTagMatcher extends TextMatcher {
  const HashTagMatcher()
      : super(r'(?<=\s|^)\#[a-zA-Z][a-zA-Z0-9]{1,}(?=\s|$)');
}
CustomText(
  'Hello world! #CustomText',
  definitions: const [
    TextDefinition(matcher: HashTagMatcher()),
  ],
  matchStyle: const TextStyle(color: Colors.lightBlue),
  onTap: (details) {
    if (details.element.matcherType == HashTagMatcher) {
      ...; 
    }
  },
)

SelectiveDefinition #

selective_definition.dart (Code / Demo)

Image - SelectiveDefinition

An example to parse a markdown-style link in the format of [shown text](url) and make it tappable.

SelectiveDefinition allows to select the string to display and the string to be passed to gesture callbacks individually from the groups of matched strings.

For details of groups, see the document of the text_parser package used internally in this package.

CustomText(
  'Tap [here](Tapped!)',
  definitions: [
    SelectiveDefinition(
      matcher: const LinkMatcher(),
      // `shownText` is used to choose the string to display.
      // It receives a list of strings that have matched the
      // fragments enclosed in parentheses within the match pattern.
      shownText: (groups) => groups[0]!,
      // `actionText` is used to choose the string to be passed
      // to the `onTap`, `onLongPress` and `onGesture` handlers.
      actionText: (groups) => groups[1]!,
    ),
  ],
  matchStyle: TextStyle(color: Colors.blue),
  hoverStyle: TextStyle(color: Colors.blue, decoration: TextDecoration.underline),
  onTap: (details) => print(details.actionText),
)

LinkMatcher is handy if used together with SelectiveDefinition, not only for making a text link but also for just decorating the bracketed strings (without showing the bracket symbols), in which case [strings]() is used as a marker to indicate which strings to be decorated.

// "def" and "jkl" are displayed in red.
CustomText(
  'abc[def]()ghi[jkl]()',
  definitions: [
    SelectiveDefinition(
      matcher: const LinkMatcher(),
      shownText: (groups) => groups[0]!,
    ),
  ],
  matchStyle: const TextStyle(color: Colors.red),
)

SpanDefinition #

span_definition.dart (Code / Demo)

Image - SpanDefinition

An example to display both text and widgets.

SpanDefinition enables a certain portion of text to be replaced with an arbitrary InlineSpan. The builder function can use the parse result (the matched string and groups) to flexibly build an InlineSpan.

Text styles, gesture handlers and the mouse cursor type are applied to the entire InlineSpan returned by the builder function. In this example, hovering is detected on all the children specified in the second SpanDefinition, i.e. the text in a TextSpan is decorated based on hoverStyle while the mouse pointer is hovering over the logo as well as over the text.

CustomText(
  'Hover and click  >>  [logo]Flutter',
  definitions: [
    SpanDefinition(
      matcher: ExactMatcher(const ['>>']),
      builder: (text, groups) => const WidgetSpan(
        child: Icon(Icons.keyboard_double_arrow_right, ...),
      ),
    ),
    SpanDefinition(
      matcher: const PatternMatcher(r'\[logo\](\w+)'),
      builder: (text, groups) => TextSpan(
        children: [
          const WidgetSpan(child: FlutterLogo()),
          const WidgetSpan(child: SizedBox(width: 2.0)),
          TextSpan(text: groups.first),
        ],
      ),
      matchStyle: TextStyle(color: Colors.blue),
      hoverStyle: TextStyle(color: Colors.blue, decoration: TextDecoration.underline),
      onTap: (details) => print(details.element.groups.first!),
    ),
  ],
)

Real hyperlinks #

real_hyperlinks.dart (Code / Demo)

Image - Real hyperlinks

An example to embed real hyperlinks for the web by making use of SpanDefinition together with the Link widget of url_launcher.

Notes:

As you can see in the screencast above, WidgetSpans are vertically misaligned with plain text on the web. It is due to issues existing on the Flutter SDK side.

CustomText(
  'Please visit [pub.dev](https://pub.dev/packages/custom_text) and ...',
  definitions: [
    SpanDefinition(
      matcher: const LinkMatcher(),
      builder: (text, groups) {
        return WidgetSpan(
          alignment: PlaceholderAlignment.middle,
          child: Link(
            uri: Uri.parse(groups[1]!),
            target: LinkTarget.blank,
            builder: (context, openLink) {
              return GestureDetector(
                onTap: openLink,
                child: Text(groups[0]!),
              );
            },
          ),
        );
      },
      matchStyle: const TextStyle(color: Colors.blue),
      hoverStyle: const TextStyle(color: Colors.blue, decoration: TextDecoration.underline),
      mouseCursor: SystemMouseCursors.click,
    ),
  ],
)

Changing mouse cursor and text style on hover #

hover_style.dart (Code / Demo)

Image - Mouse cursor and text style on hover

It is possible to change the mouse cursor type on a certain parts of text by passing a desired type to the mouseCursor parameter of a definition.

If a tap handler (onTap or onLongPress) is specified and mouseCursor is not, SystemMouseCursors.click is automatically used for the tappable element.

A different text style can also be applied on hover using hoverStyle either in CustomText or in a definition.

Tip:
Use hoverStyle and omit tapStyle if you want the same style for tap and hover.

CustomText(
  'URL: https://example.com/\n'
  'Email: foo@example.com',
  definitions: [
    const TextDefinition(
      matcher: UrlMatcher(),
      matchStyle: TextStyle(
        color: Colors.grey,
        decoration: TextDecoration.lineThrough,
      ),
      // `SystemMouseCursors.forbidden` is used for URLs.
      mouseCursor: SystemMouseCursors.forbidden,
    ),
    TextDefinition(
      matcher: const EmailMatcher(),
      matchStyle: const TextStyle(
        color: Colors.lightBlue,
        decoration: TextDecoration.underline,
      ),
      tapStyle: const TextStyle(color: Colors.green),
      // Text is shadowed while the mouse pointer hovers over it.
      hoverStyle: TextStyle(
        color: Colors.lightBlue,
        shadows: ...,
      ),
      // `SystemMouseCursors.click` is automatically used for
      // tappable elements even if `mouseCursor` is not specified.
      onTap: (details) => print(details.actionText),
    ),
  ],
)

Event positions and onGesture #

on_gesture.dart (Code / Demo)

Image - Event positions and onGesture

The onGesture handler supports events of the secondary and tertiary buttons and mouse enter and exit events.

You can check the event type with gestureKind contained in the GestureDetails object which is passed to the handler function. The object also has the global and local positions where an event happened. It is useful for showing a popup or a menu at the position.

Notes:

  • onGesture does not handle events of the primary button. Use onTap and/or onLongPress instead.
  • Unlike onTap and onLongPress, whether onGesture is specified does not affect text styling.
  • The handler function is called one microsecond or more after the actual occurrence of an event.
    • This is due to a workaround for preventing the function from being called more times than expected by updates of the text span.

CustomText.spans #

spans_constructor.dart (Code / Demo)

Image - CustomText.spans

An example of the CustomText.spans constructor that allows to use a list of InlineSpans instead of plain text.

This constructor is useful if you already have styled spans and want to decorate them additionally.

In this example, the match pattern matches the range containing multiple InlineSpans including a WidgetSpan, and the specified styles and gestures are applied to that range.

CustomText.spans(
  style: const TextStyle(fontSize: 40.0),
  definitions: [
    TextDefinition(
      // WidgetSpan is matched by `\uFFFC` or `.` in a match pattern.
      matcher: ExactMatcher(const ['Flutter devs\uFFFC']),
      matchStyle: const TextStyle(color: Colors.blue),
      hoverStyle: TextStyle(color: Colors.blue.shade300),
      mouseCursor: SystemMouseCursors.forbidden,
      onGesture: (details) => output(details.gestureKind.name),
    ),
  ],
  spans: [
    const TextSpan(text: 'Hi, '),
    const TextSpan(
      text: 'Flutter',
      style: TextStyle(
        fontWeight: FontWeight.bold,
        shadows: [Shadow(blurRadius: 4.0, color: Colors.cyan)],
      ),
    ),
    const TextSpan(text: ' devs'),
    WidgetSpan(
      alignment: PlaceholderAlignment.middle,
      child: Builder(
        builder: (context) {
          // Text style is available also in WidgetSpan via DefaultTextStyle.
          final style = DefaultTextStyle.of(context).style;
          return Icon(
            Icons.flutter_dash,
            size: style.fontSize,
            color: style.color,
          );
        },
      ),
    ),
  ],
)

Notes:

  • Arguments other than text and style in the spans passed to spans are not used even if specified.

CustomText with preBuilder #

pre_builder.dart (Code / Demo)

Image - CustomText with preBuilder

An example of preBuilder that allows to apply decorations and then additionally apply more decorations and enable gestures.

It has similar use cases to CustomText.spans, but is more helpful when it is not easy to compose complex spans manually.

The example below makes "KISS" and "Keep It Simple, Stupid!" bold, and then applies a colour to capital letters contained in them.

CustomText(
  'KISS is an acronym for "Keep It Simple, Stupid!".',
  definitions: const [
    TextDefinition(
      matcher: PatternMatcher('[A-Z]'),
      matchStyle: TextStyle(color: Colors.red),
    ),
  ],
  preBuilder: CustomSpanBuilder(
    definitions: [
      const TextDefinition(
        matcher: PatternMatcher('KISS|Keep.+Stupid!'),
        matchStyle: TextStyle(fontWeight: FontWeight.bold),
      ),
    ],
  ),
)

Notes:

  • Gesture callbacks and mouseCursor in the builder are not used even if specified.
  • The builder function is called first to parse the text and build a TextSpan, and then another parsing is performed in CustomSpan itself against the plain text converted from the built span, followed by a rebuild. Check how much it affects the performance of your app if you choose to use this.

CustomTextEditingController #

text_editing_controller.dart (Code / Demo)

Image - CustomTextEditingController

Text decoration, tap/long-press actions and hover effects are available also in an editable text field via CustomTextEditingController.

final controller = CustomTextEditingController(
  text: 'abcde foo@example.com\nhttps://example.com/ #hashtag',
  definitions: [
    const TextDefinition(
      matcher: HashTagMatcher(),
      matchStyle: TextStyle(color: Colors.orange),
      hoverStyle: TextStyle(color: Colors.red),
    ),
    ...
  ],
);
@override
Widget build(BuildContext context) {
  return TextField(
    controller: controller,
    ...,
  );
}

Notes:

  • CustomTextEditingController does not support SelectiveDefinition and SpanDefinition.
  • An error is raised on iOS simulators (not on real devices) if the initial text and onTap, onLongPress or onGesture are specified.
  • Debouncing of text parsing is available as an experimental feature for getting slightly better performance in handling long text.
    • Pass some duration to debounceDuration to enable the feature.
    • Use it at your own risk.
    • Text input will be still slow even with debouncing because Flutter itself has performance issues in editable text.

Using an external parser #

external_parser.dart (Code / Demo)

Image - External parser

It is possible to use an external parser instead of the internal one. It helps you when a different parser you already have does a better job or can do what is difficult with the default parser.

// These empty matchers are used to distinguish the matcher types of
// text elements parsed by an external parser.
// Each of the elements created by the parser needs to have the type
// of one of these matchers or `TextMatcher`. 
class KeywordMatcher extends TextMatcher {
  const KeywordMatcher() : super('');
}

class StringMatcher extends TextMatcher {
  const StringMatcher() : super('');
}
CustomText(
  sourceCode,
  parserOptions: ParserOptions.external(
    (text) => parseLanguage(text, language: 'dart'),
  ),
  definitions: [
    const TextDefinition(
      matcher: KeywordMatcher(),
      matchStyle: TextStyle(color: Colors.orange),
    ),
    const TextDefinition(
      matcher: StringMatcher(),
      matchStyle: TextStyle(color: Colors.teal),
    ),
    ...
  ],
);

Notes:

  • The external parser must generate a list of TextElements.
  • If an existing external parser creates hierarchical nodes, they need to be flattened as this package only supports a flat list.
  • If a custom parser is used with CustomTextEditingController, the TextElements generated by the parser must all together constitute the original text. Otherwise, it will cause unexpected behaviours.
    • This does not apply to CustomText.

Limitations #

  • text_parser
    • CustomText is dependent on the text_parser package made by the same author. See its documents for details if you're interested or seek troubleshooting on parsing.
105
likes
0
pub points
85%
popularity

Publisher

verified publisherkaboc.cc

Highly customisable text widget and controller to enable styling and gesture actions.

Repository (GitHub)
View/report issues

Topics

#text

License

unknown (license)

Dependencies

flutter, meta, text_parser

More

Packages that depend on custom_text