custom_text

Pub Version Flutter CI

A highly customisable text widget that allows styles and tap gestures to be applied to strings in it flexibly.

This widget is useful for making link strings tappable, such as URLs, email addresses or phone numbers, or for only highlighting partial strings in text with colors and different font settings depending on the types of the string elements parsed by regular expressions.

Examples / Usage

Most of the examples here are contained in the sample app in the example/ folder. Just click on the link below to open the web demo and see what this package can achieve.

Web Demo

The app also shows the source code with keywords highlighted, which itself is made possible by this package.

highlighting

Simplest example

example1.dart

example1

A very basic example with URLs and email addresses styled. They are not tappable 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),
  // `tapStyle` is not used if both `onTap` and `onLongPress`
  // are null or not set.
  tapStyle: const TextStyle(color: Colors.yellow),
  onTap: null,
)

Unique styles and actions per definition

example2.dart

example2

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

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

Tip: Use url_launcher or its equivalent to open a browser or another app by a tap/long-press on a string.

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(),
      // `matchStyle`, `tapStyle`, `onTap` and `onLongPress` here
      // override the equivalent parameters of CustomText.
      matchStyle: const TextStyle(
        color: Colors.green,
        decoration: TextDecoration.underline,
      ),
      tapStyle: const TextStyle(color: Colors.orange),
      onTap: (tel) => print(tel),
      onLongPress: (tel) => print('[Long press] $tel'),
    ),
  ],
  matchStyle: const TextStyle(
    color: Colors.lightBlue,
    decoration: TextDecoration.underline,
  ),
  tapStyle: const TextStyle(color: Colors.indigo),
  onTap: (type, text) => print(text),
  onLongPress: (type, text) => print('[Long press] $text'),
)

Overwriting match patterns

example3.dart

example3

An example to overwrite the default match pattern of TelMatcher.

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

CustomText(
  'Tel: +1-012-3456-7890',
  definitions: const [
    // Match patterns of preset matchers can be overwritten.
    TextDefinition(matcher: TelMatcher(r'\d{3}-\d{4}-\d{4}')),
  ],
  matchStyle: const TextStyle(color: Colors.lightBlue),
  onTap: (_, text) => print(text),
)

Custom matchers

example4.dart

example4

An example to parse hashtags using a custom matcher 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 by alphanumerics, and is enclosed with white spaces.

// You can create a custom matcher easily by extending TextMatcher.
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),
)

SelectiveDefinition

example5.dart

example5

An example to parse markdown-style links, like [label](url) using SelectiveDefinition, and make them tappable.

Each of the string shown in the widget and the string passed to the tap callbacks is selected individually from the fragments (groups) that have matched the patterns enclosed with parentheses within the match pattern.

For details of groups, see the document of the text_parser package that this package uses internally.

// This matcher comes with the package, so
// there's no need to prepare it yourself.
class LinkMatcher extends TextMatcher {
  const LinkMatcher() : super(r'\[(.+?)\]\((.*?)\)');
}
CustomText(
  'Markdown-style link\n'
  '[Tap here](Tapped!)',
  definitions: [
    SelectiveDefinition(
      matcher: const LinkMatcher(),
      // `labelSelector` is used to choose the string to show.
      // `groups` provided to `labelSelector` is an array of
      // strings matching the fragments enclosed in parentheses
      // within the match pattern.
      labelSelector: (groups) => groups[0]!,
      // `tapSelector` is used to choose the string to be passed
      // to the `onTap` and `onLongPress` callbacks.
      tapSelector: (groups) => groups[1]!,
    ),
  ],
  matchStyle: const TextStyle(color: Colors.lightBlue),
  tapStyle: const TextStyle(color: Colors.green),
  onTap: (_, text) => print(text),
)

LinkMatcher used together with SelectiveDefinition is handy not only for making a text link but also for just decorating the bracketed strings (but not showing the brackets).

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

SpanDefinition

example6.dart

example6

An example to show both strings and icons using SpanDefinition.

The builder parameter takes a function returning an InlineSpan. The function is provided with matched string and groups, so it is possible to compose an InlineSpan flexibly with them.

CustomText(
  'Email 1: foo@example.com\n'
  'Email 2: bar@example.com',
  definitions: [
    SpanDefinition(
      matcher: const EmailMatcher(),
      builder: (text, groups) => TextSpan(
        children: [
          const WidgetSpan(
            child: Icon(
              Icons.email,
              color: Colors.blueGrey,
              size: 18.0,
            ),
            alignment: PlaceholderAlignment.middle,
          ),
          const WidgetSpan(
            child: SizedBox(width: 6.0),
          ),
          TextSpan(
            text: text,
            style: const TextStyle(color: Colors.lightBlue),
            recognizer: ...,
          ),
        ],
      ),
    ),
  ],
)

Notes

  • SpanDefinition does not have arguments for styles and tap callbacks, so it is totally up to you how the InlineSpan returned from it is decorated and how it reacts to gestures.
  • The builder function is called on every rebuild. If you create a GestureRecognizer inside the function, store it in such a way that you can check if one already exists to avoid so many recognizers being created.

Changing mouse cursor and text style on hover

example7.dart

example7

TextDefinition and SelectiveDefinition have the mouseCursor property. The mouse cursor type set to it is used while the pointer hovers over a string that has matched the matcher specified in the definition.

If a tap callback (onTap or onLongPress) is set and mouseCursor is not set, SystemMouseCursors.click is automatically used for the string that the tap callback is applied to.

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

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 email addresses.
      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 used for URLs automatically
      // even if `mouseCursor` is not set because a tap has been
      // enabled by the `onTap` callback.
      onTap: (text) => output(text),
    ),
  ],
)

Limitations

  • The regular expression pattern of TelMatcher contains a lookbehind assertion, but Safari does not support it. Avoid using TelMatcher as is if your app targets Safari.
  • text_parser
    • CustomText is dependent on the text_parser package made by the same author. See its documentation for details if you're interested or for troubleshooting on parsing.

Libraries

custom_text