Line data Source code
1 : // Code Originally taken from https://github.com/Cretezy/flutter_linkify/blob/201e147e0b07b7ca5c543da8167d712d81760753/lib/flutter_linkify.dart
2 : //
3 : // Now uses local `linkify`
4 : //
5 : // Original License for this code:
6 : // MIT License
7 : // Copyright (c) 2020 Charles-William Crete
8 : //
9 : // Permission is hereby granted, free of charge, to any person obtaining a copy
10 : // of this software and associated documentation files (the "Software"), to deal
11 : // in the Software without restriction, including without limitatifon the rights
12 : // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 : // copies of the Software, and to permit persons to whom the Software is
14 : // furnished to do so, subject to the following conditions:
15 : //
16 : // The above copyright notice and this permission notice shall be included in all
17 : // copies or substantial portions of the Software.
18 : //
19 : // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 : // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 : // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 : // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 : // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 : // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 : // SOFTWARE.
26 :
27 : import 'package:flutter/gestures.dart';
28 : import 'package:flutter/material.dart';
29 :
30 : import 'linkify.dart';
31 :
32 : export 'linkify.dart' show LinkifyElement, LinkifyOptions, LinkableElement, TextElement, Linkifier, UrlElement, UrlLinkifier;
33 :
34 : /// Callback clicked link
35 : typedef LinkCallback = void Function(LinkableElement link);
36 :
37 : /// Turns URLs into links
38 : class SelectableLinkify extends StatelessWidget {
39 : /// Text to be linkified
40 : final String text;
41 :
42 : /// The number of font pixels for each logical pixel
43 : final textScaleFactor;
44 :
45 : /// Linkifiers to be used for linkify
46 : final List<Linkifier> linkifiers;
47 :
48 : /// Callback for tapping a link
49 : final LinkCallback? onOpen;
50 :
51 : /// linkify's options.
52 : final LinkifyOptions options;
53 :
54 : // TextSpan
55 :
56 : /// Style for code text
57 : final TextStyle codeStyle;
58 :
59 : /// Style for non-link text
60 : final TextStyle style;
61 :
62 : /// Style of link text
63 : final TextStyle linkStyle;
64 :
65 : // Text.rich
66 :
67 : /// How the text should be aligned horizontally.
68 : final TextAlign? textAlign;
69 :
70 : /// Text direction of the text
71 : final TextDirection? textDirection;
72 :
73 : /// The minimum number of lines to occupy when the content spans fewer lines.
74 : final int? minLines;
75 :
76 : /// The maximum number of lines for the text to span, wrapping if necessary
77 : final int? maxLines;
78 :
79 : /// The strut style used for the vertical layout
80 : final StrutStyle? strutStyle;
81 :
82 : /// Defines how to measure the width of the rendered text.
83 : final TextWidthBasis? textWidthBasis;
84 :
85 : // SelectableText.rich
86 :
87 : /// Defines the focus for this widget.
88 : final FocusNode? focusNode;
89 :
90 : /// Whether to show cursor
91 : final bool showCursor;
92 :
93 : /// Whether this text field should focus itself if nothing else is already focused.
94 : final bool autofocus;
95 :
96 : /// How thick the cursor will be
97 : final double cursorWidth;
98 :
99 : /// How rounded the corners of the cursor should be
100 : final Radius? cursorRadius;
101 :
102 : /// The color to use when painting the cursor
103 : final Color? cursorColor;
104 :
105 : /// Determines the way that drag start behavior is handled
106 : final DragStartBehavior dragStartBehavior;
107 :
108 : /// If true, then long-pressing this TextField will select text and show the cut/copy/paste menu,
109 : /// and tapping will move the text caret
110 : final bool enableInteractiveSelection;
111 :
112 : /// Called when the user taps on this selectable text (not link)
113 : final GestureTapCallback? onTap;
114 :
115 : final ScrollPhysics? scrollPhysics;
116 :
117 : /// Defines how the paragraph will apply TextStyle.height to the ascent of the first line and descent of the last line.
118 : final TextHeightBehavior? textHeightBehavior;
119 :
120 : /// How tall the cursor will be.
121 : final double? cursorHeight;
122 :
123 : /// Optional delegate for building the text selection handles and toolbar.
124 : final TextSelectionControls? selectionControls;
125 :
126 : /// Called when the user changes the selection of text (including the cursor location).
127 : final SelectionChangedCallback? onSelectionChanged;
128 :
129 : final BoxConstraints constraints;
130 :
131 0 : const SelectableLinkify({
132 : Key? key,
133 : required this.text,
134 : this.linkifiers = defaultLinkifiers,
135 : this.onOpen,
136 : this.options = const LinkifyOptions(),
137 : // TextSpan
138 : required this.style,
139 : required this.linkStyle,
140 : // RichText
141 : this.textAlign,
142 : required this.codeStyle,
143 : this.textDirection,
144 : this.minLines,
145 : this.maxLines,
146 : // SelectableText
147 : this.focusNode,
148 : this.textScaleFactor = 1.0,
149 : this.strutStyle,
150 : this.showCursor = false,
151 : this.autofocus = false,
152 : this.cursorWidth = 2.0,
153 : this.cursorRadius,
154 : this.cursorColor,
155 : this.dragStartBehavior = DragStartBehavior.start,
156 : this.enableInteractiveSelection = true,
157 : this.onTap,
158 : this.scrollPhysics,
159 : this.textWidthBasis,
160 : this.textHeightBehavior,
161 : this.cursorHeight,
162 : this.selectionControls,
163 : this.onSelectionChanged,
164 : required this.constraints,
165 0 : }) : super(key: key);
166 :
167 0 : @override
168 : Widget build(BuildContext context) {
169 0 : final elements = linkify(
170 0 : text,
171 0 : options: options,
172 0 : linkifiers: linkifiers,
173 : );
174 :
175 0 : return Container(
176 0 : constraints: constraints,
177 0 : child: SelectableText.rich(
178 0 : buildTextSpan(elements,
179 0 : style: style,
180 0 : codeStyle: codeStyle,
181 0 : onOpen: onOpen,
182 : context: context,
183 0 : linkStyle: linkStyle.copyWith(
184 : decoration: TextDecoration.underline,
185 : ),
186 0 : constraints: constraints),
187 0 : textAlign: textAlign,
188 0 : textDirection: textDirection,
189 0 : minLines: minLines,
190 0 : maxLines: maxLines,
191 0 : focusNode: focusNode,
192 0 : strutStyle: strutStyle,
193 0 : showCursor: showCursor,
194 0 : textScaleFactor: textScaleFactor,
195 0 : autofocus: autofocus,
196 0 : cursorWidth: cursorWidth,
197 0 : cursorRadius: cursorRadius,
198 0 : cursorColor: cursorColor,
199 0 : dragStartBehavior: dragStartBehavior,
200 0 : enableInteractiveSelection: enableInteractiveSelection,
201 0 : onTap: onTap,
202 0 : scrollPhysics: scrollPhysics,
203 0 : textWidthBasis: textWidthBasis,
204 0 : textHeightBehavior: textHeightBehavior,
205 0 : cursorHeight: cursorHeight,
206 0 : selectionControls: selectionControls,
207 0 : onSelectionChanged: onSelectionChanged,
208 0 : style: style,
209 : ));
210 : }
211 : }
212 :
213 : class LinkableSpan extends WidgetSpan {
214 0 : LinkableSpan({
215 : required MouseCursor mouseCursor,
216 : required InlineSpan inlineSpan,
217 : required BuildContext context,
218 0 : }) : super(
219 0 : child: MouseRegion(
220 : cursor: mouseCursor,
221 0 : child: RichText(
222 : text: inlineSpan,
223 0 : selectionRegistrar: SelectionContainer.maybeOf(context),
224 : ),
225 : ),
226 : );
227 : }
228 :
229 : /// Raw TextSpan builder for more control on the RichText
230 0 : TextSpan buildTextSpan(
231 : List<LinkifyElement> elements, {
232 : TextStyle? style,
233 : TextStyle? linkStyle,
234 : TextStyle? codeStyle,
235 : LinkCallback? onOpen,
236 : required BuildContext context,
237 : bool useMouseRegion = false,
238 : required BoxConstraints constraints,
239 : }) {
240 : // Ok, so the problem here is that Flutter really wants to optimize this function
241 : // out of the rebuild process. This is fine when the screen gets smaller because
242 : // Flutter forces TextSpan to rebuild with the new constraints automatically.
243 : // HOWEVER, when the screen gets larger, Flutter seems to think that it doesn't
244 : // need to bother rebuilding this TextSpan because it already fits in the provided constraints.
245 : // To force a rebuild here we append a constraint-determined space character to the end of the
246 : // text element.
247 : // (I tried a few other things, including the docs-sanctioned MediaQuery.sizeOf(context) - which promises a rebuild
248 : // but Flutter is pretty good at optimizing "useless" checks out)
249 : String inlineText = "\u0020";
250 0 : if (constraints.maxWidth % 2 == 0) {
251 : inlineText = "\u00A0";
252 : }
253 0 : elements.add(TextElement(inlineText));
254 0 : return TextSpan(
255 : style: style,
256 0 : children: elements.map<InlineSpan>(
257 0 : (element) {
258 0 : if (element is LinkableElement) {
259 : if (useMouseRegion) {
260 0 : return TextSpan(
261 0 : text: element.text,
262 : style: linkStyle,
263 : mouseCursor: SystemMouseCursors.click,
264 0 : recognizer: onOpen != null ? (TapGestureRecognizer()..onTap = () => onOpen(element)) : null,
265 0 : semanticsLabel: element.text);
266 : } else {
267 0 : return TextSpan(
268 0 : text: element.text,
269 : style: linkStyle,
270 0 : recognizer: onOpen != null ? (TapGestureRecognizer()..onTap = () => onOpen(element)) : null,
271 : );
272 : }
273 0 : } else if (element is BoldElement) {
274 0 : return TextSpan(text: element.text.replaceAll("*", ""), style: style?.copyWith(fontWeight: FontWeight.bold), semanticsLabel: element.text);
275 0 : } else if (element is ItalicElement) {
276 0 : return TextSpan(text: element.text.replaceAll("*", ""), style: style?.copyWith(fontStyle: FontStyle.italic), semanticsLabel: element.text);
277 0 : } else if (element is SuperElement) {
278 0 : return WidgetSpan(
279 0 : child: Transform.translate(
280 : offset: const Offset(2, -6),
281 0 : child: Text(element.text.replaceAll("^", ""),
282 : //superscript is usually smaller in size
283 : textScaleFactor: 0.7,
284 : style: style,
285 0 : semanticsLabel: element.text),
286 : ));
287 0 : } else if (element is SubElement) {
288 0 : return WidgetSpan(
289 0 : child: Transform.translate(
290 : offset: const Offset(2, 4),
291 0 : child: Text(element.text.replaceAll("_", ""),
292 : //superscript is usually smaller in size
293 : textScaleFactor: 0.7,
294 : style: style,
295 0 : semanticsLabel: element.text),
296 : ));
297 0 : } else if (element is StrikeElement) {
298 0 : return TextSpan(
299 0 : text: element.text.replaceAll("~~", ""),
300 0 : style: style?.copyWith(decoration: TextDecoration.lineThrough, decorationColor: style.color, decorationStyle: TextDecorationStyle.solid),
301 0 : semanticsLabel: element.text);
302 0 : } else if (element is CodeElement) {
303 0 : return TextSpan(
304 0 : text: element.text.replaceAll("\`", ""),
305 : // monospace fonts at the same size as regular text makes them appear
306 : // slightly larger, so we compensate by making them slightly smaller...
307 0 : style: codeStyle?.copyWith(fontFamily: "RobotoMono", fontSize: codeStyle.fontSize! - 1.5),
308 0 : semanticsLabel: element.text);
309 : } else {
310 0 : return TextSpan(
311 0 : text: element.text,
312 : style: style,
313 : );
314 : }
315 : },
316 0 : ).toList(),
317 : );
318 : }
319 :
320 : // Show a tooltip over an inlined element in a Rich Text widget.
321 : class TooltipSpan extends WidgetSpan {
322 0 : TooltipSpan({
323 : required String message,
324 : required InlineSpan inlineSpan,
325 : required BuildContext context,
326 0 : }) : super(
327 0 : child: Tooltip(
328 : message: message,
329 0 : child: RichText(
330 : text: inlineSpan,
331 0 : selectionRegistrar: SelectionContainer.maybeOf(context),
332 : ),
333 : ),
334 : );
335 : }
|