1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'color_scheme.dart';
6/// @docImport 'scaffold.dart';
7/// @docImport 'selection_area.dart';
8/// @docImport 'text_field.dart';
9library;
10
11import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle;
12
13import 'package:flutter/cupertino.dart';
14import 'package:flutter/foundation.dart';
15import 'package:flutter/gestures.dart';
16import 'package:flutter/rendering.dart';
17import 'package:flutter/scheduler.dart';
18
19import 'adaptive_text_selection_toolbar.dart';
20import 'desktop_text_selection.dart';
21import 'magnifier.dart';
22import 'text_selection.dart';
23import 'theme.dart';
24
25// Examples can assume:
26// late BuildContext context;
27// late FocusNode myFocusNode;
28
29/// An eyeballed value that moves the cursor slightly left of where it is
30/// rendered for text on Android so its positioning more accurately matches the
31/// native iOS text cursor positioning.
32///
33/// This value is in device pixels, not logical pixels as is typically used
34/// throughout the codebase.
35const int iOSHorizontalOffset = -2;
36
37class _TextSpanEditingController extends TextEditingController {
38 _TextSpanEditingController({required TextSpan textSpan})
39 : _textSpan = textSpan,
40 super(text: textSpan.toPlainText(includeSemanticsLabels: false));
41
42 final TextSpan _textSpan;
43
44 @override
45 TextSpan buildTextSpan({
46 required BuildContext context,
47 TextStyle? style,
48 required bool withComposing,
49 }) {
50 // This does not care about composing.
51 return TextSpan(style: style, children: <TextSpan>[_textSpan]);
52 }
53
54 @override
55 set text(String? newText) {
56 // This should never be reached.
57 throw UnimplementedError();
58 }
59}
60
61class _SelectableTextSelectionGestureDetectorBuilder extends TextSelectionGestureDetectorBuilder {
62 _SelectableTextSelectionGestureDetectorBuilder({required _SelectableTextState state})
63 : _state = state,
64 super(delegate: state);
65
66 final _SelectableTextState _state;
67
68 @override
69 void onSingleTapUp(TapDragUpDetails details) {
70 if (!delegate.selectionEnabled) {
71 return;
72 }
73 super.onSingleTapUp(details);
74 _state.widget.onTap?.call();
75 }
76}
77
78/// A run of selectable text with a single style.
79///
80/// Consider using [SelectionArea] or [SelectableRegion] instead, which enable
81/// selection on a widget subtree, including but not limited to [Text] widgets.
82///
83/// The [SelectableText] widget displays a string of text with a single style.
84/// The string might break across multiple lines or might all be displayed on
85/// the same line depending on the layout constraints.
86///
87/// {@youtube 560 315 https://www.youtube.com/watch?v=ZSU3ZXOs6hc}
88///
89/// The [style] argument is optional. When omitted, the text will use the style
90/// from the closest enclosing [DefaultTextStyle]. If the given style's
91/// [TextStyle.inherit] property is true (the default), the given style will
92/// be merged with the closest enclosing [DefaultTextStyle]. This merging
93/// behavior is useful, for example, to make the text bold while using the
94/// default font family and size.
95///
96/// {@macro flutter.material.textfield.wantKeepAlive}
97///
98/// {@tool snippet}
99///
100/// ```dart
101/// const SelectableText(
102/// 'Hello! How are you?',
103/// textAlign: TextAlign.center,
104/// style: TextStyle(fontWeight: FontWeight.bold),
105/// )
106/// ```
107/// {@end-tool}
108///
109/// Using the [SelectableText.rich] constructor, the [SelectableText] widget can
110/// display a paragraph with differently styled [TextSpan]s. The sample
111/// that follows displays "Hello beautiful world" with different styles
112/// for each word.
113///
114/// {@tool snippet}
115///
116/// ```dart
117/// const SelectableText.rich(
118/// TextSpan(
119/// text: 'Hello', // default text style
120/// children: <TextSpan>[
121/// TextSpan(text: ' beautiful ', style: TextStyle(fontStyle: FontStyle.italic)),
122/// TextSpan(text: 'world', style: TextStyle(fontWeight: FontWeight.bold)),
123/// ],
124/// ),
125/// )
126/// ```
127/// {@end-tool}
128///
129/// ## Interactivity
130///
131/// To make [SelectableText] react to touch events, use callback [onTap] to achieve
132/// the desired behavior.
133///
134/// ## Scrolling Considerations
135///
136/// If this [SelectableText] is not a descendant of [Scaffold] and is being used
137/// within a [Scrollable] or nested [Scrollable]s, consider placing a
138/// [ScrollNotificationObserver] above the root [Scrollable] that contains this
139/// [SelectableText] to ensure proper scroll coordination for [SelectableText]
140/// and its components like [TextSelectionOverlay].
141///
142/// See also:
143///
144/// * [Text], which is the non selectable version of this widget.
145/// * [TextField], which is the editable version of this widget.
146/// * [SelectionArea], which enables the selection of multiple [Text] widgets
147/// and of other widgets.
148class SelectableText extends StatefulWidget {
149 /// Creates a selectable text widget.
150 ///
151 /// If the [style] argument is null, the text will use the style from the
152 /// closest enclosing [DefaultTextStyle].
153 ///
154
155 /// If the [showCursor], [autofocus], [dragStartBehavior],
156 /// [selectionHeightStyle], [selectionWidthStyle] and [data] arguments are
157 /// specified, the [maxLines] argument must be greater than zero.
158 const SelectableText(
159 String this.data, {
160 super.key,
161 this.focusNode,
162 this.style,
163 this.strutStyle,
164 this.textAlign,
165 this.textDirection,
166 @Deprecated(
167 'Use textScaler instead. '
168 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
169 'This feature was deprecated after v3.12.0-2.0.pre.',
170 )
171 this.textScaleFactor,
172 this.textScaler,
173 this.showCursor = false,
174 this.autofocus = false,
175 @Deprecated(
176 'Use `contextMenuBuilder` instead. '
177 'This feature was deprecated after v3.3.0-0.5.pre.',
178 )
179 this.toolbarOptions,
180 this.minLines,
181 this.maxLines,
182 this.cursorWidth = 2.0,
183 this.cursorHeight,
184 this.cursorRadius,
185 this.cursorColor,
186 this.selectionColor,
187 this.selectionHeightStyle = ui.BoxHeightStyle.tight,
188 this.selectionWidthStyle = ui.BoxWidthStyle.tight,
189 this.dragStartBehavior = DragStartBehavior.start,
190 this.enableInteractiveSelection = true,
191 this.selectionControls,
192 this.onTap,
193 this.scrollPhysics,
194 this.scrollBehavior,
195 this.semanticsLabel,
196 this.textHeightBehavior,
197 this.textWidthBasis,
198 this.onSelectionChanged,
199 this.contextMenuBuilder = _defaultContextMenuBuilder,
200 this.magnifierConfiguration,
201 }) : assert(maxLines == null || maxLines > 0),
202 assert(minLines == null || minLines > 0),
203 assert(
204 (maxLines == null) || (minLines == null) || (maxLines >= minLines),
205 "minLines can't be greater than maxLines",
206 ),
207 assert(
208 textScaler == null || textScaleFactor == null,
209 'textScaleFactor is deprecated and cannot be specified when textScaler is specified.',
210 ),
211 textSpan = null;
212
213 /// Creates a selectable text widget with a [TextSpan].
214 ///
215 /// The [TextSpan.children] attribute of the [textSpan] parameter must only
216 /// contain [TextSpan]s. Other types of [InlineSpan] are not allowed.
217 const SelectableText.rich(
218 TextSpan this.textSpan, {
219 super.key,
220 this.focusNode,
221 this.style,
222 this.strutStyle,
223 this.textAlign,
224 this.textDirection,
225 @Deprecated(
226 'Use textScaler instead. '
227 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
228 'This feature was deprecated after v3.12.0-2.0.pre.',
229 )
230 this.textScaleFactor,
231 this.textScaler,
232 this.showCursor = false,
233 this.autofocus = false,
234 @Deprecated(
235 'Use `contextMenuBuilder` instead. '
236 'This feature was deprecated after v3.3.0-0.5.pre.',
237 )
238 this.toolbarOptions,
239 this.minLines,
240 this.maxLines,
241 this.cursorWidth = 2.0,
242 this.cursorHeight,
243 this.cursorRadius,
244 this.cursorColor,
245 this.selectionColor,
246 this.selectionHeightStyle = ui.BoxHeightStyle.tight,
247 this.selectionWidthStyle = ui.BoxWidthStyle.tight,
248 this.dragStartBehavior = DragStartBehavior.start,
249 this.enableInteractiveSelection = true,
250 this.selectionControls,
251 this.onTap,
252 this.scrollPhysics,
253 this.scrollBehavior,
254 this.semanticsLabel,
255 this.textHeightBehavior,
256 this.textWidthBasis,
257 this.onSelectionChanged,
258 this.contextMenuBuilder = _defaultContextMenuBuilder,
259 this.magnifierConfiguration,
260 }) : assert(maxLines == null || maxLines > 0),
261 assert(minLines == null || minLines > 0),
262 assert(
263 (maxLines == null) || (minLines == null) || (maxLines >= minLines),
264 "minLines can't be greater than maxLines",
265 ),
266 assert(
267 textScaler == null || textScaleFactor == null,
268 'textScaleFactor is deprecated and cannot be specified when textScaler is specified.',
269 ),
270 data = null;
271
272 /// The text to display.
273 ///
274 /// This will be null if a [textSpan] is provided instead.
275 final String? data;
276
277 /// The text to display as a [TextSpan].
278 ///
279 /// This will be null if [data] is provided instead.
280 final TextSpan? textSpan;
281
282 /// Defines the focus for this widget.
283 ///
284 /// Text is only selectable when widget is focused.
285 ///
286 /// The [focusNode] is a long-lived object that's typically managed by a
287 /// [StatefulWidget] parent. See [FocusNode] for more information.
288 ///
289 /// To give the focus to this widget, provide a [focusNode] and then
290 /// use the current [FocusScope] to request the focus:
291 ///
292 /// ```dart
293 /// FocusScope.of(context).requestFocus(myFocusNode);
294 /// ```
295 ///
296 /// This happens automatically when the widget is tapped.
297 ///
298 /// To be notified when the widget gains or loses the focus, add a listener
299 /// to the [focusNode]:
300 ///
301 /// ```dart
302 /// myFocusNode.addListener(() { print(myFocusNode.hasFocus); });
303 /// ```
304 ///
305 /// If null, this widget will create its own [FocusNode] with
306 /// [FocusNode.skipTraversal] parameter set to `true`, which causes the widget
307 /// to be skipped over during focus traversal.
308 final FocusNode? focusNode;
309
310 /// The style to use for the text.
311 ///
312 /// If null, defaults [DefaultTextStyle] of context.
313 final TextStyle? style;
314
315 /// {@macro flutter.widgets.editableText.strutStyle}
316 final StrutStyle? strutStyle;
317
318 /// {@macro flutter.widgets.editableText.textAlign}
319 final TextAlign? textAlign;
320
321 /// {@macro flutter.widgets.editableText.textDirection}
322 final TextDirection? textDirection;
323
324 /// {@macro flutter.widgets.editableText.textScaleFactor}
325 @Deprecated(
326 'Use textScaler instead. '
327 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
328 'This feature was deprecated after v3.12.0-2.0.pre.',
329 )
330 final double? textScaleFactor;
331
332 /// {@macro flutter.painting.textPainter.textScaler}
333 final TextScaler? textScaler;
334
335 /// {@macro flutter.widgets.editableText.autofocus}
336 final bool autofocus;
337
338 /// {@macro flutter.widgets.editableText.minLines}
339 final int? minLines;
340
341 /// {@macro flutter.widgets.editableText.maxLines}
342 final int? maxLines;
343
344 /// {@macro flutter.widgets.editableText.showCursor}
345 final bool showCursor;
346
347 /// {@macro flutter.widgets.editableText.cursorWidth}
348 final double cursorWidth;
349
350 /// {@macro flutter.widgets.editableText.cursorHeight}
351 final double? cursorHeight;
352
353 /// {@macro flutter.widgets.editableText.cursorRadius}
354 final Radius? cursorRadius;
355
356 /// The color of the cursor.
357 ///
358 /// The cursor indicates the current text insertion point.
359 ///
360 /// If null then [DefaultSelectionStyle.cursorColor] is used. If that is also
361 /// null and [ThemeData.platform] is [TargetPlatform.iOS] or
362 /// [TargetPlatform.macOS], then [CupertinoThemeData.primaryColor] is used.
363 /// Otherwise [ColorScheme.primary] of [ThemeData.colorScheme] is used.
364 final Color? cursorColor;
365
366 /// The color to use when painting the selection.
367 ///
368 /// If this property is null, this widget gets the selection color from the
369 /// inherited [DefaultSelectionStyle] (if any); if none, the selection
370 /// color is derived from the [CupertinoThemeData.primaryColor] on
371 /// Apple platforms and [ColorScheme.primary] of [ThemeData.colorScheme] on
372 /// other platforms.
373 final Color? selectionColor;
374
375 /// Controls how tall the selection highlight boxes are computed to be.
376 ///
377 /// See [ui.BoxHeightStyle] for details on available styles.
378 final ui.BoxHeightStyle selectionHeightStyle;
379
380 /// Controls how wide the selection highlight boxes are computed to be.
381 ///
382 /// See [ui.BoxWidthStyle] for details on available styles.
383 final ui.BoxWidthStyle selectionWidthStyle;
384
385 /// {@macro flutter.widgets.editableText.enableInteractiveSelection}
386 final bool enableInteractiveSelection;
387
388 /// {@macro flutter.widgets.editableText.selectionControls}
389 final TextSelectionControls? selectionControls;
390
391 /// {@macro flutter.widgets.scrollable.dragStartBehavior}
392 final DragStartBehavior dragStartBehavior;
393
394 /// Configuration of toolbar options.
395 ///
396 /// Paste and cut will be disabled regardless.
397 ///
398 /// If not set, select all and copy will be enabled by default.
399 @Deprecated(
400 'Use `contextMenuBuilder` instead. '
401 'This feature was deprecated after v3.3.0-0.5.pre.',
402 )
403 final ToolbarOptions? toolbarOptions;
404
405 /// {@macro flutter.widgets.editableText.selectionEnabled}
406 bool get selectionEnabled => enableInteractiveSelection;
407
408 /// Called when the user taps on this selectable text.
409 ///
410 /// The selectable text builds a [GestureDetector] to handle input events like tap,
411 /// to trigger focus requests, to move the caret, adjust the selection, etc.
412 /// Handling some of those events by wrapping the selectable text with a competing
413 /// GestureDetector is problematic.
414 ///
415 /// To unconditionally handle taps, without interfering with the selectable text's
416 /// internal gesture detector, provide this callback.
417 ///
418 /// To be notified when the text field gains or loses the focus, provide a
419 /// [focusNode] and add a listener to that.
420 ///
421 /// To listen to arbitrary pointer events without competing with the
422 /// selectable text's internal gesture detector, use a [Listener].
423 final GestureTapCallback? onTap;
424
425 /// {@macro flutter.widgets.editableText.scrollPhysics}
426 final ScrollPhysics? scrollPhysics;
427
428 /// {@macro flutter.widgets.editableText.scrollBehavior}
429 final ScrollBehavior? scrollBehavior;
430
431 /// {@macro flutter.widgets.Text.semanticsLabel}
432 final String? semanticsLabel;
433
434 /// {@macro dart.ui.textHeightBehavior}
435 final TextHeightBehavior? textHeightBehavior;
436
437 /// {@macro flutter.painting.textPainter.textWidthBasis}
438 final TextWidthBasis? textWidthBasis;
439
440 /// {@macro flutter.widgets.editableText.onSelectionChanged}
441 final SelectionChangedCallback? onSelectionChanged;
442
443 /// {@macro flutter.widgets.EditableText.contextMenuBuilder}
444 final EditableTextContextMenuBuilder? contextMenuBuilder;
445
446 static Widget _defaultContextMenuBuilder(
447 BuildContext context,
448 EditableTextState editableTextState,
449 ) {
450 if (defaultTargetPlatform == TargetPlatform.iOS && SystemContextMenu.isSupported(context)) {
451 return SystemContextMenu.editableText(editableTextState: editableTextState);
452 }
453 return AdaptiveTextSelectionToolbar.editableText(editableTextState: editableTextState);
454 }
455
456 /// The configuration for the magnifier used when the text is selected.
457 ///
458 /// By default, builds a [CupertinoTextMagnifier] on iOS and [TextMagnifier]
459 /// on Android, and builds nothing on all other platforms. To suppress the
460 /// magnifier, consider passing [TextMagnifierConfiguration.disabled].
461 ///
462 /// {@macro flutter.widgets.magnifier.intro}
463 final TextMagnifierConfiguration? magnifierConfiguration;
464
465 @override
466 State<SelectableText> createState() => _SelectableTextState();
467
468 @override
469 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
470 super.debugFillProperties(properties);
471 properties.add(DiagnosticsProperty<String>('data', data, defaultValue: null));
472 properties.add(
473 DiagnosticsProperty<String>('semanticsLabel', semanticsLabel, defaultValue: null),
474 );
475 properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode, defaultValue: null));
476 properties.add(DiagnosticsProperty<TextStyle>('style', style, defaultValue: null));
477 properties.add(DiagnosticsProperty<bool>('autofocus', autofocus, defaultValue: false));
478 properties.add(DiagnosticsProperty<bool>('showCursor', showCursor, defaultValue: false));
479 properties.add(IntProperty('minLines', minLines, defaultValue: null));
480 properties.add(IntProperty('maxLines', maxLines, defaultValue: null));
481 properties.add(EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: null));
482 properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
483 properties.add(DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: null));
484 properties.add(DiagnosticsProperty<TextScaler>('textScaler', textScaler, defaultValue: null));
485 properties.add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0));
486 properties.add(DoubleProperty('cursorHeight', cursorHeight, defaultValue: null));
487 properties.add(DiagnosticsProperty<Radius>('cursorRadius', cursorRadius, defaultValue: null));
488 properties.add(DiagnosticsProperty<Color>('cursorColor', cursorColor, defaultValue: null));
489 properties.add(
490 DiagnosticsProperty<Color>('selectionColor', selectionColor, defaultValue: null),
491 );
492 properties.add(
493 FlagProperty(
494 'selectionEnabled',
495 value: selectionEnabled,
496 defaultValue: true,
497 ifFalse: 'selection disabled',
498 ),
499 );
500 properties.add(
501 DiagnosticsProperty<TextSelectionControls>(
502 'selectionControls',
503 selectionControls,
504 defaultValue: null,
505 ),
506 );
507 properties.add(
508 DiagnosticsProperty<ScrollPhysics>('scrollPhysics', scrollPhysics, defaultValue: null),
509 );
510 properties.add(
511 DiagnosticsProperty<ScrollBehavior>('scrollBehavior', scrollBehavior, defaultValue: null),
512 );
513 properties.add(
514 DiagnosticsProperty<TextHeightBehavior>(
515 'textHeightBehavior',
516 textHeightBehavior,
517 defaultValue: null,
518 ),
519 );
520 }
521}
522
523class _SelectableTextState extends State<SelectableText>
524 implements TextSelectionGestureDetectorBuilderDelegate {
525 EditableTextState? get _editableText => editableTextKey.currentState;
526
527 late _TextSpanEditingController _controller;
528
529 FocusNode? _focusNode;
530 FocusNode get _effectiveFocusNode =>
531 widget.focusNode ?? (_focusNode ??= FocusNode(skipTraversal: true));
532
533 bool _showSelectionHandles = false;
534
535 late _SelectableTextSelectionGestureDetectorBuilder _selectionGestureDetectorBuilder;
536
537 // API for TextSelectionGestureDetectorBuilderDelegate.
538 @override
539 late bool forcePressEnabled;
540
541 @override
542 final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>();
543
544 @override
545 bool get selectionEnabled => widget.selectionEnabled;
546 // End of API for TextSelectionGestureDetectorBuilderDelegate.
547
548 @override
549 void initState() {
550 super.initState();
551 _selectionGestureDetectorBuilder = _SelectableTextSelectionGestureDetectorBuilder(state: this);
552 _controller = _TextSpanEditingController(
553 textSpan: widget.textSpan ?? TextSpan(text: widget.data),
554 );
555 _controller.addListener(_onControllerChanged);
556 _effectiveFocusNode.addListener(_handleFocusChanged);
557 }
558
559 @override
560 void didUpdateWidget(SelectableText oldWidget) {
561 super.didUpdateWidget(oldWidget);
562 if (widget.data != oldWidget.data || widget.textSpan != oldWidget.textSpan) {
563 _controller.removeListener(_onControllerChanged);
564 _controller.dispose();
565 _controller = _TextSpanEditingController(
566 textSpan: widget.textSpan ?? TextSpan(text: widget.data),
567 );
568 _controller.addListener(_onControllerChanged);
569 }
570 if (widget.focusNode != oldWidget.focusNode) {
571 (oldWidget.focusNode ?? _focusNode)?.removeListener(_handleFocusChanged);
572 (widget.focusNode ?? _focusNode)?.addListener(_handleFocusChanged);
573 }
574 if (_effectiveFocusNode.hasFocus && _controller.selection.isCollapsed) {
575 _showSelectionHandles = false;
576 } else {
577 _showSelectionHandles = true;
578 }
579 }
580
581 @override
582 void dispose() {
583 _effectiveFocusNode.removeListener(_handleFocusChanged);
584 _focusNode?.dispose();
585 _controller.dispose();
586 super.dispose();
587 }
588
589 void _onControllerChanged() {
590 final bool showSelectionHandles =
591 !_effectiveFocusNode.hasFocus || !_controller.selection.isCollapsed;
592 if (showSelectionHandles == _showSelectionHandles) {
593 return;
594 }
595 setState(() {
596 _showSelectionHandles = showSelectionHandles;
597 });
598 }
599
600 void _handleFocusChanged() {
601 if (!_effectiveFocusNode.hasFocus &&
602 SchedulerBinding.instance.lifecycleState == AppLifecycleState.resumed) {
603 // We should only clear the selection when this SelectableText loses
604 // focus while the application is currently running. It is possible
605 // that the application is not currently running, for example on desktop
606 // platforms, clicking on a different window switches the focus to
607 // the new window causing the Flutter application to go inactive. In this
608 // case we want to retain the selection so it remains when we return to
609 // the Flutter application.
610 _controller.value = TextEditingValue(text: _controller.value.text);
611 }
612 }
613
614 void _handleSelectionChanged(TextSelection selection, SelectionChangedCause? cause) {
615 final bool willShowSelectionHandles = _shouldShowSelectionHandles(cause);
616 if (willShowSelectionHandles != _showSelectionHandles) {
617 setState(() {
618 _showSelectionHandles = willShowSelectionHandles;
619 });
620 }
621
622 widget.onSelectionChanged?.call(selection, cause);
623
624 switch (Theme.of(context).platform) {
625 case TargetPlatform.iOS:
626 case TargetPlatform.macOS:
627 if (cause == SelectionChangedCause.longPress) {
628 _editableText?.bringIntoView(selection.base);
629 }
630 return;
631 case TargetPlatform.android:
632 case TargetPlatform.fuchsia:
633 case TargetPlatform.linux:
634 case TargetPlatform.windows:
635 // Do nothing.
636 }
637 }
638
639 /// Toggle the toolbar when a selection handle is tapped.
640 void _handleSelectionHandleTapped() {
641 if (_controller.selection.isCollapsed) {
642 _editableText!.toggleToolbar();
643 }
644 }
645
646 bool _shouldShowSelectionHandles(SelectionChangedCause? cause) {
647 // When the text field is activated by something that doesn't trigger the
648 // selection overlay, we shouldn't show the handles either.
649 if (!_selectionGestureDetectorBuilder.shouldShowSelectionToolbar) {
650 return false;
651 }
652
653 if (_controller.selection.isCollapsed) {
654 return false;
655 }
656
657 if (cause == SelectionChangedCause.keyboard) {
658 return false;
659 }
660
661 if (cause == SelectionChangedCause.longPress) {
662 return true;
663 }
664
665 if (_controller.text.isNotEmpty) {
666 return true;
667 }
668
669 return false;
670 }
671
672 @override
673 Widget build(BuildContext context) {
674 // TODO(garyq): Assert to block WidgetSpans from being used here are removed,
675 // but we still do not yet have nice handling of things like carets, clipboard,
676 // and other features. We should add proper support. Currently, caret handling
677 // is blocked on SkParagraph switch and https://github.com/flutter/engine/pull/27010
678 // should be landed in SkParagraph after the switch is complete.
679 assert(debugCheckHasMediaQuery(context));
680 assert(debugCheckHasDirectionality(context));
681 assert(
682 !(widget.style != null &&
683 !widget.style!.inherit &&
684 (widget.style!.fontSize == null || widget.style!.textBaseline == null)),
685 'inherit false style must supply fontSize and textBaseline',
686 );
687
688 final ThemeData theme = Theme.of(context);
689 final DefaultSelectionStyle selectionStyle = DefaultSelectionStyle.of(context);
690 final FocusNode focusNode = _effectiveFocusNode;
691
692 TextSelectionControls? textSelectionControls = widget.selectionControls;
693 final bool paintCursorAboveText;
694 final bool cursorOpacityAnimates;
695 Offset? cursorOffset;
696 final Color cursorColor;
697 final Color selectionColor;
698 Radius? cursorRadius = widget.cursorRadius;
699
700 switch (theme.platform) {
701 case TargetPlatform.iOS:
702 final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context);
703 forcePressEnabled = true;
704 textSelectionControls ??= cupertinoTextSelectionHandleControls;
705 paintCursorAboveText = true;
706 cursorOpacityAnimates = true;
707 cursorColor =
708 widget.cursorColor ?? selectionStyle.cursorColor ?? cupertinoTheme.primaryColor;
709 selectionColor =
710 selectionStyle.selectionColor ?? cupertinoTheme.primaryColor.withOpacity(0.40);
711 cursorRadius ??= const Radius.circular(2.0);
712 cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.devicePixelRatioOf(context), 0);
713
714 case TargetPlatform.macOS:
715 final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context);
716 forcePressEnabled = false;
717 textSelectionControls ??= cupertinoDesktopTextSelectionHandleControls;
718 paintCursorAboveText = true;
719 cursorOpacityAnimates = true;
720 cursorColor =
721 widget.cursorColor ?? selectionStyle.cursorColor ?? cupertinoTheme.primaryColor;
722 selectionColor =
723 selectionStyle.selectionColor ?? cupertinoTheme.primaryColor.withOpacity(0.40);
724 cursorRadius ??= const Radius.circular(2.0);
725 cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.devicePixelRatioOf(context), 0);
726
727 case TargetPlatform.android:
728 case TargetPlatform.fuchsia:
729 forcePressEnabled = false;
730 textSelectionControls ??= materialTextSelectionHandleControls;
731 paintCursorAboveText = false;
732 cursorOpacityAnimates = false;
733 cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? theme.colorScheme.primary;
734 selectionColor =
735 selectionStyle.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40);
736
737 case TargetPlatform.linux:
738 case TargetPlatform.windows:
739 forcePressEnabled = false;
740 textSelectionControls ??= desktopTextSelectionHandleControls;
741 paintCursorAboveText = false;
742 cursorOpacityAnimates = false;
743 cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? theme.colorScheme.primary;
744 selectionColor =
745 selectionStyle.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40);
746 }
747
748 final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context);
749 TextStyle? effectiveTextStyle = widget.style;
750 if (effectiveTextStyle == null || effectiveTextStyle.inherit) {
751 effectiveTextStyle = defaultTextStyle.style.merge(
752 widget.style ?? _controller._textSpan.style,
753 );
754 }
755 final TextScaler? effectiveScaler =
756 widget.textScaler ??
757 switch (widget.textScaleFactor) {
758 null => null,
759 final double textScaleFactor => TextScaler.linear(textScaleFactor),
760 };
761 final Widget child = RepaintBoundary(
762 child: EditableText(
763 key: editableTextKey,
764 style: effectiveTextStyle,
765 readOnly: true,
766 toolbarOptions: widget.toolbarOptions,
767 textWidthBasis: widget.textWidthBasis ?? defaultTextStyle.textWidthBasis,
768 textHeightBehavior: widget.textHeightBehavior ?? defaultTextStyle.textHeightBehavior,
769 showSelectionHandles: _showSelectionHandles,
770 showCursor: widget.showCursor,
771 controller: _controller,
772 focusNode: focusNode,
773 strutStyle: widget.strutStyle ?? const StrutStyle(),
774 textAlign: widget.textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start,
775 textDirection: widget.textDirection,
776 textScaler: effectiveScaler,
777 autofocus: widget.autofocus,
778 forceLine: false,
779 minLines: widget.minLines,
780 maxLines: widget.maxLines ?? defaultTextStyle.maxLines,
781 selectionColor: widget.selectionColor ?? selectionColor,
782 selectionControls: widget.selectionEnabled ? textSelectionControls : null,
783 onSelectionChanged: _handleSelectionChanged,
784 onSelectionHandleTapped: _handleSelectionHandleTapped,
785 rendererIgnoresPointer: true,
786 cursorWidth: widget.cursorWidth,
787 cursorHeight: widget.cursorHeight,
788 cursorRadius: cursorRadius,
789 cursorColor: cursorColor,
790 selectionHeightStyle: widget.selectionHeightStyle,
791 selectionWidthStyle: widget.selectionWidthStyle,
792 cursorOpacityAnimates: cursorOpacityAnimates,
793 cursorOffset: cursorOffset,
794 paintCursorAboveText: paintCursorAboveText,
795 backgroundCursorColor: CupertinoColors.inactiveGray,
796 enableInteractiveSelection: widget.enableInteractiveSelection,
797 magnifierConfiguration:
798 widget.magnifierConfiguration ?? TextMagnifier.adaptiveMagnifierConfiguration,
799 dragStartBehavior: widget.dragStartBehavior,
800 scrollPhysics: widget.scrollPhysics,
801 scrollBehavior: widget.scrollBehavior,
802 autofillHints: null,
803 contextMenuBuilder: widget.contextMenuBuilder,
804 ),
805 );
806
807 return Semantics(
808 label: widget.semanticsLabel,
809 excludeSemantics: widget.semanticsLabel != null,
810 onLongPress: () {
811 _effectiveFocusNode.requestFocus();
812 },
813 child: _selectionGestureDetectorBuilder.buildGestureDetector(
814 behavior: HitTestBehavior.translucent,
815 child: child,
816 ),
817 );
818 }
819}
820

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com