cwtch-ui/lib/third_party/linkify/flutter_linkify.dart

336 lines
11 KiB
Dart

// 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<Linkifier> 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<LinkifyElement> 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<InlineSpan>(
(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),
),
),
);
}