// Code Originally taken from https://github.com/Cretezy/flutter_linkify/blob/201e147e0b07b7ca5c543da8167d712d81760753/lib/flutter_linkify.dart // // Now uses local `linkify` // // Original License for this code: // MIT License // Copyright (c) 2020 Charles-William Crete // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitatifon the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'linkify.dart'; export 'linkify.dart' show LinkifyElement, LinkifyOptions, LinkableElement, TextElement, Linkifier, UrlElement, UrlLinkifier; /// Callback clicked link typedef LinkCallback = void Function(LinkableElement link); /// Turns URLs into links class SelectableLinkify extends StatelessWidget { /// Text to be linkified final String text; /// The number of font pixels for each logical pixel final textScaleFactor; /// Linkifiers to be used for linkify final List linkifiers; /// Callback for tapping a link final LinkCallback? onOpen; /// linkify's options. final LinkifyOptions options; // TextSpan /// Style for code text final TextStyle codeStyle; /// Style for non-link text final TextStyle style; /// Style of link text final TextStyle linkStyle; // Text.rich /// How the text should be aligned horizontally. final TextAlign? textAlign; /// Text direction of the text final TextDirection? textDirection; /// The minimum number of lines to occupy when the content spans fewer lines. final int? minLines; /// The maximum number of lines for the text to span, wrapping if necessary final int? maxLines; /// The strut style used for the vertical layout final StrutStyle? strutStyle; /// Defines how to measure the width of the rendered text. final TextWidthBasis? textWidthBasis; // SelectableText.rich /// Defines the focus for this widget. final FocusNode? focusNode; /// Whether to show cursor final bool showCursor; /// Whether this text field should focus itself if nothing else is already focused. final bool autofocus; /// How thick the cursor will be final double cursorWidth; /// How rounded the corners of the cursor should be final Radius? cursorRadius; /// The color to use when painting the cursor final Color? cursorColor; /// Determines the way that drag start behavior is handled final DragStartBehavior dragStartBehavior; /// If true, then long-pressing this TextField will select text and show the cut/copy/paste menu, /// and tapping will move the text caret final bool enableInteractiveSelection; /// Called when the user taps on this selectable text (not link) final GestureTapCallback? onTap; final ScrollPhysics? scrollPhysics; /// Defines how the paragraph will apply TextStyle.height to the ascent of the first line and descent of the last line. final TextHeightBehavior? textHeightBehavior; /// How tall the cursor will be. final double? cursorHeight; /// Optional delegate for building the text selection handles and toolbar. final TextSelectionControls? selectionControls; /// Called when the user changes the selection of text (including the cursor location). final SelectionChangedCallback? onSelectionChanged; final BoxConstraints? constraints; const SelectableLinkify({ Key? key, required this.text, this.linkifiers = defaultLinkifiers, this.onOpen, this.options = const LinkifyOptions(), // TextSpan required this.style, required this.linkStyle, // RichText this.textAlign, required this.codeStyle, this.textDirection, this.minLines, this.maxLines, // SelectableText this.focusNode, this.textScaleFactor = 1.0, this.strutStyle, this.showCursor = false, this.autofocus = false, this.cursorWidth = 2.0, this.cursorRadius, this.cursorColor, this.dragStartBehavior = DragStartBehavior.start, this.enableInteractiveSelection = true, this.onTap, this.scrollPhysics, this.textWidthBasis, this.textHeightBehavior, this.cursorHeight, this.selectionControls, this.onSelectionChanged, this.constraints, }) : super(key: key); @override Widget build(BuildContext context) { final elements = linkify( text, options: options, linkifiers: linkifiers, ); return Container( constraints: constraints, child: SelectableText.rich( buildTextSpan(elements, style: style, codeStyle: codeStyle, onOpen: onOpen, context: context, linkStyle: linkStyle.copyWith( decoration: TextDecoration.underline, ), constraints: constraints), textAlign: textAlign, textDirection: textDirection, minLines: minLines, maxLines: maxLines, focusNode: focusNode, strutStyle: strutStyle, showCursor: showCursor, textScaleFactor: textScaleFactor, autofocus: autofocus, cursorWidth: cursorWidth, cursorRadius: cursorRadius, cursorColor: cursorColor, dragStartBehavior: dragStartBehavior, enableInteractiveSelection: enableInteractiveSelection, onTap: onTap, scrollPhysics: scrollPhysics, textWidthBasis: textWidthBasis, textHeightBehavior: textHeightBehavior, cursorHeight: cursorHeight, selectionControls: selectionControls, onSelectionChanged: onSelectionChanged, style: style, )); } } class LinkableSpan extends WidgetSpan { LinkableSpan({ required MouseCursor mouseCursor, required InlineSpan inlineSpan, required BuildContext context, }) : super( child: MouseRegion( cursor: mouseCursor, child: RichText( text: inlineSpan, selectionRegistrar: SelectionContainer.maybeOf(context), ), ), ); } /// Raw TextSpan builder for more control on the RichText TextSpan buildTextSpan( List elements, { TextStyle? style, TextStyle? linkStyle, TextStyle? codeStyle, LinkCallback? onOpen, required BuildContext context, bool useMouseRegion = false, BoxConstraints? constraints, }) { // Ok, so the problem here is that Flutter really wants to optimize this function // out of the rebuild process. This is fine when the screen gets smaller because // Flutter forces TextSpan to rebuild with the new constraints automatically. // HOWEVER, when the screen gets larger, Flutter seems to think that it doesn't // need to bother rebuilding this TextSpan because it already fits in the provided constraints. // To force a rebuild here we append a constraint-determined space character to the end of the // text element. // (I tried a few other things, including the docs-sanctioned MediaQuery.sizeOf(context) - which promises a rebuild // but Flutter is pretty good at optimizing "useless" checks out) String inlineText = "\u0020"; if (constraints != null && constraints.maxWidth % 2 == 0) { inlineText = "\u00A0"; } elements.add(TextElement(inlineText)); return TextSpan( style: style, children: elements.map( (element) { if (element is LinkableElement) { if (useMouseRegion) { return TextSpan( text: element.text, style: linkStyle, mouseCursor: SystemMouseCursors.click, recognizer: onOpen != null ? (TapGestureRecognizer()..onTap = () => onOpen(element)) : null, semanticsLabel: element.text); } else { return TextSpan( text: element.text, style: linkStyle, recognizer: onOpen != null ? (TapGestureRecognizer()..onTap = () => onOpen(element)) : null, ); } } else if (element is BoldElement) { return TextSpan(text: element.text.replaceAll("*", ""), style: style?.copyWith(fontWeight: FontWeight.bold), semanticsLabel: element.text); } else if (element is ItalicElement) { return TextSpan(text: element.text.replaceAll("*", ""), style: style?.copyWith(fontStyle: FontStyle.italic), semanticsLabel: element.text); } else if (element is SuperElement) { return WidgetSpan( child: Transform.translate( offset: const Offset(2, -6), child: Text(element.text.replaceAll("^", ""), //superscript is usually smaller in size textScaleFactor: 0.7, style: style, semanticsLabel: element.text), )); } else if (element is SubElement) { return WidgetSpan( child: Transform.translate( offset: const Offset(2, 4), child: Text(element.text.replaceAll("_", ""), //superscript is usually smaller in size textScaleFactor: 0.7, style: style, semanticsLabel: element.text), )); } else if (element is StrikeElement) { return TextSpan( text: element.text.replaceAll("~~", ""), style: style?.copyWith(decoration: TextDecoration.lineThrough, decorationColor: style.color, decorationStyle: TextDecorationStyle.solid), semanticsLabel: element.text); } else if (element is CodeElement) { return TextSpan( text: element.text.replaceAll("\`", ""), // monospace fonts at the same size as regular text makes them appear // slightly larger, so we compensate by making them slightly smaller... style: codeStyle?.copyWith(fontFamily: "RobotoMono", fontSize: codeStyle.fontSize! - 1.5), semanticsLabel: element.text); } else { return TextSpan( text: element.text, style: style, ); } }, ).toList(), ); } // Show a tooltip over an inlined element in a Rich Text widget. class TooltipSpan extends WidgetSpan { TooltipSpan({ required String message, required InlineSpan inlineSpan, required BuildContext context, }) : super( child: Tooltip( message: message, child: RichText( text: inlineSpan, selectionRegistrar: SelectionContainer.maybeOf(context), ), ), ); }