custom_text 1.0.0-dev.1 copy "custom_text: ^1.0.0-dev.1" to clipboard
custom_text: ^1.0.0-dev.1 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 gestures on strings, and a special TextEditingController that makes most of the functionality of CustomText available in an editable text field too.

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 examples #

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 what this package can do.

Web Demo

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

Code highlighting

Simplest example #

example1.dart (Code / Demo)

example1

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

The coloured strings 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),
)

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.

Unique styles and actions per definition #

example2.dart (Code / Demo)

example2

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 url_launcher 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 pattern #

example3.dart (Code / Demo)

example3

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 pattern #

example4.dart (Code / Demo)

example4

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 #

example5.dart (Code / Demo)

example5

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

Each of the string shown in the widget and the string passed to the tap handlers is selected 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(
  'Markdown-style link\n'
  '[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: const TextStyle(color: Colors.lightBlue),
  tapStyle: const TextStyle(color: Colors.green),
  onTap: (details) => print(details.actionText),
)

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

// "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 #

example6.dart (Code / Demo)

example6

An example to display both strings and icons using SpanDefinition.

The builder parameter takes a function returning an InlineSpan. The function can use the matching string and groups passed to it to compose an InlineSpan flexibly with them.

CustomText(
  'Email 1: [@] foo@example.com\n'
  'Email 2: [@] bar@example.com',
  definitions: [
    SpanDefinition(
      matcher: const PatternMatcher(r'\[@\]'),
      builder: (text, groups) => const WidgetSpan(
        child: Icon(
          Icons.email,
          color: Colors.blueGrey,
          size: 20.0,
        ),
      ),
    ),
    const TextDefinition(
      matcher: EmailMatcher(),
      matchStyle: TextStyle(color: Colors.lightBlue),
      onTap: (details) => print(details.actionText),
    ),
  ],
)

Notes:

  • SpanDefinition does not have arguments for styles and tap handlers, so it depends entirely on how the InlineSpan is configured.
  • The builder function is called on every rebuild. If you use GestureRecognizer to make a WidgetSpan tappable, be careful not to create it inside the function, or make sure to dispose of existing recognizers before creating a new one.

Changing mouse cursor and text style on hover #

example7.dart (Code / Demo)

example7

TextDefinition and SelectiveDefinition have the mouseCursor property. The mouse cursor type passed to it is used for a string that has matched the match pattern while the pointer hovers over it.

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 position and onGesture #

example8.dart (Code / Demo)

example8

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.

CustomTextEditingController #

example9.dart (Code / Demo)

example9

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. It only accepts TextDefinition.
  • 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.

External parser #

example10.dart (Code / Demo)

example10

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.
// The parser needs to specify one of these matchers in each element. 
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 #

  • 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 documents for details if you're interested or seek troubleshooting on parsing.
87
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

License

unknown (LICENSE)

Dependencies

flutter, meta, text_parser

More

Packages that depend on custom_text