selectable_text.dart 26.1 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle;

7 8 9
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
10
import 'package:flutter/rendering.dart';
11

12
import 'adaptive_text_selection_toolbar.dart';
13
import 'desktop_text_selection.dart';
14
import 'feedback.dart';
15
import 'magnifier.dart';
16 17 18
import 'text_selection.dart';
import 'theme.dart';

19 20 21 22
// Examples can assume:
// late BuildContext context;
// late FocusNode myFocusNode;

23 24 25 26 27 28 29 30 31
/// An eyeballed value that moves the cursor slightly left of where it is
/// rendered for text on Android so its positioning more accurately matches the
/// native iOS text cursor positioning.
///
/// This value is in device pixels, not logical pixels as is typically used
/// throughout the codebase.
const int iOSHorizontalOffset = -2;

class _TextSpanEditingController extends TextEditingController {
32
  _TextSpanEditingController({required TextSpan textSpan}):
33
    _textSpan = textSpan,
34
    super(text: textSpan.toPlainText(includeSemanticsLabels: false));
35 36 37 38

  final TextSpan _textSpan;

  @override
39
  TextSpan buildTextSpan({required BuildContext context, TextStyle? style, required bool withComposing}) {
40
    // This does not care about composing.
41 42 43 44 45 46 47
    return TextSpan(
      style: style,
      children: <TextSpan>[_textSpan],
    );
  }

  @override
48
  set text(String? newText) {
49 50
    // This should never be reached.
    throw UnimplementedError();
51 52 53 54 55
  }
}

class _SelectableTextSelectionGestureDetectorBuilder extends TextSelectionGestureDetectorBuilder {
  _SelectableTextSelectionGestureDetectorBuilder({
56
    required _SelectableTextState state,
57
  }) : _state = state,
58
       super(delegate: state);
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77

  final _SelectableTextState _state;

  @override
  void onForcePressStart(ForcePressDetails details) {
    super.onForcePressStart(details);
    if (delegate.selectionEnabled && shouldShowSelectionToolbar) {
      editableText.showToolbar();
    }
  }

  @override
  void onForcePressEnd(ForcePressDetails details) {
    // Not required.
  }

  @override
  void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) {
    if (delegate.selectionEnabled) {
78 79 80 81 82
      renderEditable.selectWordsInRange(
        from: details.globalPosition - details.offsetFromOrigin,
        to: details.globalPosition,
        cause: SelectionChangedCause.longPress,
      );
83 84 85 86
    }
  }

  @override
87
  void onSingleTapUp(TapDragUpDetails details) {
88 89
    editableText.hideToolbar();
    if (delegate.selectionEnabled) {
90
      switch (Theme.of(_state.context).platform) {
91
        case TargetPlatform.iOS:
92
        case TargetPlatform.macOS:
93 94 95
          renderEditable.selectWordEdge(cause: SelectionChangedCause.tap);
        case TargetPlatform.android:
        case TargetPlatform.fuchsia:
96 97
        case TargetPlatform.linux:
        case TargetPlatform.windows:
98 99 100
          renderEditable.selectPosition(cause: SelectionChangedCause.tap);
      }
    }
101
    _state.widget.onTap?.call();
102 103 104 105 106
  }

  @override
  void onSingleLongTapStart(LongPressStartDetails details) {
    if (delegate.selectionEnabled) {
107 108
      renderEditable.selectWord(cause: SelectionChangedCause.longPress);
      Feedback.forLongPress(_state.context);
109 110 111 112 113 114 115 116 117 118
    }
  }
}

/// A run of selectable text with a single style.
///
/// The [SelectableText] widget displays a string of text with a single style.
/// The string might break across multiple lines or might all be displayed on
/// the same line depending on the layout constraints.
///
119 120
/// {@youtube 560 315 https://www.youtube.com/watch?v=ZSU3ZXOs6hc}
///
121 122 123 124 125 126 127
/// The [style] argument is optional. When omitted, the text will use the style
/// from the closest enclosing [DefaultTextStyle]. If the given style's
/// [TextStyle.inherit] property is true (the default), the given style will
/// be merged with the closest enclosing [DefaultTextStyle]. This merging
/// behavior is useful, for example, to make the text bold while using the
/// default font family and size.
///
128 129
/// {@macro flutter.material.textfield.wantKeepAlive}
///
130
/// {@tool snippet}
131 132
///
/// ```dart
133
/// const SelectableText(
134 135 136 137 138 139 140 141 142 143 144 145
///   'Hello! How are you?',
///   textAlign: TextAlign.center,
///   style: TextStyle(fontWeight: FontWeight.bold),
/// )
/// ```
/// {@end-tool}
///
/// Using the [SelectableText.rich] constructor, the [SelectableText] widget can
/// display a paragraph with differently styled [TextSpan]s. The sample
/// that follows displays "Hello beautiful world" with different styles
/// for each word.
///
146
/// {@tool snippet}
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175
///
/// ```dart
/// const SelectableText.rich(
///   TextSpan(
///     text: 'Hello', // default text style
///     children: <TextSpan>[
///       TextSpan(text: ' beautiful ', style: TextStyle(fontStyle: FontStyle.italic)),
///       TextSpan(text: 'world', style: TextStyle(fontWeight: FontWeight.bold)),
///     ],
///   ),
/// )
/// ```
/// {@end-tool}
///
/// ## Interactivity
///
/// To make [SelectableText] react to touch events, use callback [onTap] to achieve
/// the desired behavior.
///
/// See also:
///
///  * [Text], which is the non selectable version of this widget.
///  * [TextField], which is the editable version of this widget.
class SelectableText extends StatefulWidget {
  /// Creates a selectable text widget.
  ///
  /// If the [style] argument is null, the text will use the style from the
  /// closest enclosing [DefaultTextStyle].
  ///
176

177 178 179
  /// The [showCursor], [autofocus], [dragStartBehavior], [selectionHeightStyle],
  /// [selectionWidthStyle] and [data] parameters must not be null. If specified,
  /// the [maxLines] argument must be greater than zero.
180
  const SelectableText(
181
    String this.data, {
182
    super.key,
183 184 185 186 187
    this.focusNode,
    this.style,
    this.strutStyle,
    this.textAlign,
    this.textDirection,
188
    this.textScaleFactor,
189 190
    this.showCursor = false,
    this.autofocus = false,
191 192 193 194 195
    @Deprecated(
      'Use `contextMenuBuilder` instead. '
      'This feature was deprecated after v3.3.0-0.5.pre.',
    )
    this.toolbarOptions,
196
    this.minLines,
197 198
    this.maxLines,
    this.cursorWidth = 2.0,
199
    this.cursorHeight,
200 201
    this.cursorRadius,
    this.cursorColor,
202 203
    this.selectionHeightStyle = ui.BoxHeightStyle.tight,
    this.selectionWidthStyle = ui.BoxWidthStyle.tight,
204 205
    this.dragStartBehavior = DragStartBehavior.start,
    this.enableInteractiveSelection = true,
206
    this.selectionControls,
207 208
    this.onTap,
    this.scrollPhysics,
209
    this.semanticsLabel,
210
    this.textHeightBehavior,
211
    this.textWidthBasis,
212
    this.onSelectionChanged,
213
    this.contextMenuBuilder = _defaultContextMenuBuilder,
214
    this.magnifierConfiguration,
215
  }) :  assert(maxLines == null || maxLines > 0),
216 217 218
        assert(minLines == null || minLines > 0),
        assert(
          (maxLines == null) || (minLines == null) || (maxLines >= minLines),
219
          "minLines can't be greater than maxLines",
220
        ),
221
        textSpan = null;
222 223 224 225

  /// Creates a selectable text widget with a [TextSpan].
  ///
  /// The [textSpan] parameter must not be null and only contain [TextSpan] in
226
  /// [textSpan].children. Other type of [InlineSpan] is not allowed.
227 228
  ///
  /// The [autofocus] and [dragStartBehavior] arguments must not be null.
229
  const SelectableText.rich(
230
    TextSpan this.textSpan, {
231
    super.key,
232 233 234 235 236
    this.focusNode,
    this.style,
    this.strutStyle,
    this.textAlign,
    this.textDirection,
237
    this.textScaleFactor,
238 239
    this.showCursor = false,
    this.autofocus = false,
240 241 242 243 244
    @Deprecated(
      'Use `contextMenuBuilder` instead. '
      'This feature was deprecated after v3.3.0-0.5.pre.',
    )
    this.toolbarOptions,
245
    this.minLines,
246 247
    this.maxLines,
    this.cursorWidth = 2.0,
248
    this.cursorHeight,
249 250
    this.cursorRadius,
    this.cursorColor,
251 252
    this.selectionHeightStyle = ui.BoxHeightStyle.tight,
    this.selectionWidthStyle = ui.BoxWidthStyle.tight,
253 254
    this.dragStartBehavior = DragStartBehavior.start,
    this.enableInteractiveSelection = true,
255
    this.selectionControls,
256 257
    this.onTap,
    this.scrollPhysics,
258
    this.semanticsLabel,
259
    this.textHeightBehavior,
260
    this.textWidthBasis,
261
    this.onSelectionChanged,
262
    this.contextMenuBuilder = _defaultContextMenuBuilder,
263
    this.magnifierConfiguration,
264
  }) :  assert(maxLines == null || maxLines > 0),
265 266 267
    assert(minLines == null || minLines > 0),
    assert(
      (maxLines == null) || (minLines == null) || (maxLines >= minLines),
268
      "minLines can't be greater than maxLines",
269
    ),
270
    data = null;
271 272 273 274

  /// The text to display.
  ///
  /// This will be null if a [textSpan] is provided instead.
275
  final String? data;
276 277 278 279

  /// The text to display as a [TextSpan].
  ///
  /// This will be null if [data] is provided instead.
280
  final TextSpan? textSpan;
281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301

  /// Defines the focus for this widget.
  ///
  /// Text is only selectable when widget is focused.
  ///
  /// The [focusNode] is a long-lived object that's typically managed by a
  /// [StatefulWidget] parent. See [FocusNode] for more information.
  ///
  /// To give the focus to this widget, provide a [focusNode] and then
  /// use the current [FocusScope] to request the focus:
  ///
  /// ```dart
  /// FocusScope.of(context).requestFocus(myFocusNode);
  /// ```
  ///
  /// This happens automatically when the widget is tapped.
  ///
  /// To be notified when the widget gains or loses the focus, add a listener
  /// to the [focusNode]:
  ///
  /// ```dart
302
  /// myFocusNode.addListener(() { print(myFocusNode.hasFocus); });
303 304
  /// ```
  ///
305 306 307
  /// If null, this widget will create its own [FocusNode] with
  /// [FocusNode.skipTraversal] parameter set to `true`, which causes the widget
  /// to be skipped over during focus traversal.
308
  final FocusNode? focusNode;
309 310 311 312

  /// The style to use for the text.
  ///
  /// 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
  final double? textScaleFactor;
326

327 328 329
  /// {@macro flutter.widgets.editableText.autofocus}
  final bool autofocus;

330
  /// {@macro flutter.widgets.editableText.minLines}
331
  final int? minLines;
332

333
  /// {@macro flutter.widgets.editableText.maxLines}
334
  final int? maxLines;
335 336 337 338 339 340 341

  /// {@macro flutter.widgets.editableText.showCursor}
  final bool showCursor;

  /// {@macro flutter.widgets.editableText.cursorWidth}
  final double cursorWidth;

342
  /// {@macro flutter.widgets.editableText.cursorHeight}
343
  final double? cursorHeight;
344

345
  /// {@macro flutter.widgets.editableText.cursorRadius}
346
  final Radius? cursorRadius;
347

348
  /// The color of the cursor.
349
  ///
350 351 352 353 354 355
  /// The cursor indicates the current text insertion point.
  ///
  /// If null then [DefaultSelectionStyle.cursorColor] is used. If that is also
  /// null and [ThemeData.platform] is [TargetPlatform.iOS] or
  /// [TargetPlatform.macOS], then [CupertinoThemeData.primaryColor] is used.
  /// Otherwise [ColorScheme.primary] of [ThemeData.colorScheme] is used.
356
  final Color? cursorColor;
357

358 359 360 361 362 363 364 365 366 367
  /// Controls how tall the selection highlight boxes are computed to be.
  ///
  /// See [ui.BoxHeightStyle] for details on available styles.
  final ui.BoxHeightStyle selectionHeightStyle;

  /// Controls how wide the selection highlight boxes are computed to be.
  ///
  /// See [ui.BoxWidthStyle] for details on available styles.
  final ui.BoxWidthStyle selectionWidthStyle;

368 369 370
  /// {@macro flutter.widgets.editableText.enableInteractiveSelection}
  final bool enableInteractiveSelection;

371 372 373
  /// {@macro flutter.widgets.editableText.selectionControls}
  final TextSelectionControls? selectionControls;

374 375 376
  /// {@macro flutter.widgets.scrollable.dragStartBehavior}
  final DragStartBehavior dragStartBehavior;

377 378 379 380 381
  /// Configuration of toolbar options.
  ///
  /// Paste and cut will be disabled regardless.
  ///
  /// If not set, select all and copy will be enabled by default.
382 383 384 385 386
  @Deprecated(
    'Use `contextMenuBuilder` instead. '
    'This feature was deprecated after v3.3.0-0.5.pre.',
  )
  final ToolbarOptions? toolbarOptions;
387

388 389
  /// {@macro flutter.widgets.editableText.selectionEnabled}
  bool get selectionEnabled => enableInteractiveSelection;
390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405

  /// Called when the user taps on this selectable text.
  ///
  /// The selectable text builds a [GestureDetector] to handle input events like tap,
  /// to trigger focus requests, to move the caret, adjust the selection, etc.
  /// Handling some of those events by wrapping the selectable text with a competing
  /// GestureDetector is problematic.
  ///
  /// To unconditionally handle taps, without interfering with the selectable text's
  /// internal gesture detector, provide this callback.
  ///
  /// To be notified when the text field gains or loses the focus, provide a
  /// [focusNode] and add a listener to that.
  ///
  /// To listen to arbitrary pointer events without competing with the
  /// selectable text's internal gesture detector, use a [Listener].
406
  final GestureTapCallback? onTap;
407

Dan Field's avatar
Dan Field committed
408
  /// {@macro flutter.widgets.editableText.scrollPhysics}
409
  final ScrollPhysics? scrollPhysics;
410

411 412 413
  /// {@macro flutter.widgets.Text.semanticsLabel}
  final String? semanticsLabel;

414
  /// {@macro dart.ui.textHeightBehavior}
415
  final TextHeightBehavior? textHeightBehavior;
416

417
  /// {@macro flutter.painting.textPainter.textWidthBasis}
418
  final TextWidthBasis? textWidthBasis;
419

420
  /// {@macro flutter.widgets.editableText.onSelectionChanged}
421
  final SelectionChangedCallback? onSelectionChanged;
422

423 424 425 426 427 428 429 430 431
  /// {@macro flutter.widgets.EditableText.contextMenuBuilder}
  final EditableTextContextMenuBuilder? contextMenuBuilder;

  static Widget _defaultContextMenuBuilder(BuildContext context, EditableTextState editableTextState) {
    return AdaptiveTextSelectionToolbar.editableText(
      editableTextState: editableTextState,
    );
  }

432
  /// {@macro flutter.widgets.magnifier.TextMagnifierConfiguration.intro}
433 434 435
  ///
  /// {@macro flutter.widgets.magnifier.intro}
  ///
436
  /// {@macro flutter.widgets.magnifier.TextMagnifierConfiguration.details}
437
  ///
438 439 440
  /// By default, builds a [CupertinoTextMagnifier] on iOS and [TextMagnifier]
  /// on Android, and builds nothing on all other platforms. If it is desired to
  /// suppress the magnifier, consider passing [TextMagnifierConfiguration.disabled].
441 442
  final TextMagnifierConfiguration? magnifierConfiguration;

443
  @override
444
  State<SelectableText> createState() => _SelectableTextState();
445 446 447 448 449

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DiagnosticsProperty<String>('data', data, defaultValue: null));
450
    properties.add(DiagnosticsProperty<String>('semanticsLabel', semanticsLabel, defaultValue: null));
451 452 453 454
    properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode, defaultValue: null));
    properties.add(DiagnosticsProperty<TextStyle>('style', style, defaultValue: null));
    properties.add(DiagnosticsProperty<bool>('autofocus', autofocus, defaultValue: false));
    properties.add(DiagnosticsProperty<bool>('showCursor', showCursor, defaultValue: false));
455
    properties.add(IntProperty('minLines', minLines, defaultValue: null));
456 457 458
    properties.add(IntProperty('maxLines', maxLines, defaultValue: null));
    properties.add(EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: null));
    properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
459
    properties.add(DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: null));
460
    properties.add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0));
461
    properties.add(DoubleProperty('cursorHeight', cursorHeight, defaultValue: null));
462 463 464
    properties.add(DiagnosticsProperty<Radius>('cursorRadius', cursorRadius, defaultValue: null));
    properties.add(DiagnosticsProperty<Color>('cursorColor', cursorColor, defaultValue: null));
    properties.add(FlagProperty('selectionEnabled', value: selectionEnabled, defaultValue: true, ifFalse: 'selection disabled'));
465
    properties.add(DiagnosticsProperty<TextSelectionControls>('selectionControls', selectionControls, defaultValue: null));
466
    properties.add(DiagnosticsProperty<ScrollPhysics>('scrollPhysics', scrollPhysics, defaultValue: null));
467
    properties.add(DiagnosticsProperty<TextHeightBehavior>('textHeightBehavior', textHeightBehavior, defaultValue: null));
468 469 470
  }
}

471
class _SelectableTextState extends State<SelectableText> implements TextSelectionGestureDetectorBuilderDelegate {
472
  EditableTextState? get _editableText => editableTextKey.currentState;
473

474
  late _TextSpanEditingController _controller;
475

476
  FocusNode? _focusNode;
477 478
  FocusNode get _effectiveFocusNode =>
      widget.focusNode ?? (_focusNode ??= FocusNode(skipTraversal: true));
479 480 481

  bool _showSelectionHandles = false;

482
  late _SelectableTextSelectionGestureDetectorBuilder _selectionGestureDetectorBuilder;
483 484 485

  // API for TextSelectionGestureDetectorBuilderDelegate.
  @override
486
  late bool forcePressEnabled;
487 488 489 490 491 492 493 494 495 496 497

  @override
  final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>();

  @override
  bool get selectionEnabled => widget.selectionEnabled;
  // End of API for TextSelectionGestureDetectorBuilderDelegate.

  @override
  void initState() {
    super.initState();
498 499 500
    _selectionGestureDetectorBuilder = _SelectableTextSelectionGestureDetectorBuilder(
      state: this,
    );
501
    _controller = _TextSpanEditingController(
502
        textSpan: widget.textSpan ?? TextSpan(text: widget.data),
503
    );
504
    _controller.addListener(_onControllerChanged);
505 506 507 508 509 510
  }

  @override
  void didUpdateWidget(SelectableText oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.data != oldWidget.data || widget.textSpan != oldWidget.textSpan) {
511
      _controller.removeListener(_onControllerChanged);
512
      _controller = _TextSpanEditingController(
513
          textSpan: widget.textSpan ?? TextSpan(text: widget.data),
514
      );
515
      _controller.addListener(_onControllerChanged);
516 517
    }
    if (_effectiveFocusNode.hasFocus && _controller.selection.isCollapsed) {
518
      _showSelectionHandles = false;
519 520
    } else {
      _showSelectionHandles = true;
521 522 523 524 525 526
    }
  }

  @override
  void dispose() {
    _focusNode?.dispose();
527
    _controller.dispose();
528 529 530
    super.dispose();
  }

531 532 533 534 535 536 537 538 539 540 541
  void _onControllerChanged() {
    final bool showSelectionHandles = !_effectiveFocusNode.hasFocus
      || !_controller.selection.isCollapsed;
    if (showSelectionHandles == _showSelectionHandles) {
      return;
    }
    setState(() {
      _showSelectionHandles = showSelectionHandles;
    });
  }

542
  void _handleSelectionChanged(TextSelection selection, SelectionChangedCause? cause) {
543 544 545 546 547 548
    final bool willShowSelectionHandles = _shouldShowSelectionHandles(cause);
    if (willShowSelectionHandles != _showSelectionHandles) {
      setState(() {
        _showSelectionHandles = willShowSelectionHandles;
      });
    }
549 550

    widget.onSelectionChanged?.call(selection, cause);
551

552
    switch (Theme.of(context).platform) {
553
      case TargetPlatform.iOS:
554
      case TargetPlatform.macOS:
555 556 557 558 559 560
        if (cause == SelectionChangedCause.longPress) {
          _editableText?.bringIntoView(selection.base);
        }
        return;
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
561 562
      case TargetPlatform.linux:
      case TargetPlatform.windows:
563 564 565 566 567 568 569
      // Do nothing.
    }
  }

  /// Toggle the toolbar when a selection handle is tapped.
  void _handleSelectionHandleTapped() {
    if (_controller.selection.isCollapsed) {
570
      _editableText!.toggleToolbar();
571 572 573
    }
  }

574
  bool _shouldShowSelectionHandles(SelectionChangedCause? cause) {
575 576
    // When the text field is activated by something that doesn't trigger the
    // selection overlay, we shouldn't show the handles either.
577
    if (!_selectionGestureDetectorBuilder.shouldShowSelectionToolbar) {
578
      return false;
579
    }
580

581
    if (_controller.selection.isCollapsed) {
582
      return false;
583
    }
584

585
    if (cause == SelectionChangedCause.keyboard) {
586
      return false;
587
    }
588

589
    if (cause == SelectionChangedCause.longPress) {
590
      return true;
591
    }
592

593
    if (_controller.text.isNotEmpty) {
594
      return true;
595
    }
596 597 598 599 600 601

    return false;
  }

  @override
  Widget build(BuildContext context) {
602 603 604 605 606
    // TODO(garyq): Assert to block WidgetSpans from being used here are removed,
    // but we still do not yet have nice handling of things like carets, clipboard,
    // and other features. We should add proper support. Currently, caret handling
    // is blocked on SkParagraph switch and https://github.com/flutter/engine/pull/27010
    // should be landed in SkParagraph after the switch is complete.
607 608 609
    assert(debugCheckHasMediaQuery(context));
    assert(debugCheckHasDirectionality(context));
    assert(
610
      !(widget.style != null && !widget.style!.inherit &&
611
          (widget.style!.fontSize == null || widget.style!.textBaseline == null)),
612 613 614
      'inherit false style must supply fontSize and textBaseline',
    );

615
    final ThemeData theme = Theme.of(context);
616
    final DefaultSelectionStyle selectionStyle = DefaultSelectionStyle.of(context);
617 618
    final FocusNode focusNode = _effectiveFocusNode;

619
    TextSelectionControls? textSelectionControls =  widget.selectionControls;
620 621
    final bool paintCursorAboveText;
    final bool cursorOpacityAnimates;
622
    Offset? cursorOffset;
623
    final Color cursorColor;
624
    final Color selectionColor;
625
    Radius? cursorRadius = widget.cursorRadius;
626

627
    switch (theme.platform) {
628
      case TargetPlatform.iOS:
629
        final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context);
630
        forcePressEnabled = true;
631
        textSelectionControls ??= cupertinoTextSelectionHandleControls;
632 633
        paintCursorAboveText = true;
        cursorOpacityAnimates = true;
634 635
        cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? cupertinoTheme.primaryColor;
        selectionColor = selectionStyle.selectionColor ?? cupertinoTheme.primaryColor.withOpacity(0.40);
636
        cursorRadius ??= const Radius.circular(2.0);
637
        cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.devicePixelRatioOf(context), 0);
638 639 640 641

      case TargetPlatform.macOS:
        final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context);
        forcePressEnabled = false;
642
        textSelectionControls ??= cupertinoDesktopTextSelectionHandleControls;
643 644
        paintCursorAboveText = true;
        cursorOpacityAnimates = true;
645 646
        cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? cupertinoTheme.primaryColor;
        selectionColor = selectionStyle.selectionColor ?? cupertinoTheme.primaryColor.withOpacity(0.40);
647
        cursorRadius ??= const Radius.circular(2.0);
648
        cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.devicePixelRatioOf(context), 0);
649 650 651

      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
652
        forcePressEnabled = false;
653
        textSelectionControls ??= materialTextSelectionHandleControls;
654 655
        paintCursorAboveText = false;
        cursorOpacityAnimates = false;
656 657
        cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? theme.colorScheme.primary;
        selectionColor = selectionStyle.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40);
658

659 660
      case TargetPlatform.linux:
      case TargetPlatform.windows:
661
        forcePressEnabled = false;
662
        textSelectionControls ??= desktopTextSelectionHandleControls;
663 664
        paintCursorAboveText = false;
        cursorOpacityAnimates = false;
665 666
        cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? theme.colorScheme.primary;
        selectionColor = selectionStyle.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40);
667 668 669
    }

    final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context);
670
    TextStyle? effectiveTextStyle = widget.style;
671
    if (effectiveTextStyle == null || effectiveTextStyle.inherit) {
672
      effectiveTextStyle = defaultTextStyle.style.merge(widget.style ?? _controller._textSpan.style);
673
    }
674 675 676 677 678
    final Widget child = RepaintBoundary(
      child: EditableText(
        key: editableTextKey,
        style: effectiveTextStyle,
        readOnly: true,
679
        toolbarOptions: widget.toolbarOptions,
680
        textWidthBasis: widget.textWidthBasis ?? defaultTextStyle.textWidthBasis,
681
        textHeightBehavior: widget.textHeightBehavior ?? defaultTextStyle.textHeightBehavior,
682 683 684 685
        showSelectionHandles: _showSelectionHandles,
        showCursor: widget.showCursor,
        controller: _controller,
        focusNode: focusNode,
686
        strutStyle: widget.strutStyle ?? const StrutStyle(),
687 688
        textAlign: widget.textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start,
        textDirection: widget.textDirection,
689
        textScaleFactor: widget.textScaleFactor,
690 691
        autofocus: widget.autofocus,
        forceLine: false,
692
        minLines: widget.minLines,
693
        maxLines: widget.maxLines ?? defaultTextStyle.maxLines,
694
        selectionColor: selectionColor,
695 696 697 698 699
        selectionControls: widget.selectionEnabled ? textSelectionControls : null,
        onSelectionChanged: _handleSelectionChanged,
        onSelectionHandleTapped: _handleSelectionHandleTapped,
        rendererIgnoresPointer: true,
        cursorWidth: widget.cursorWidth,
700
        cursorHeight: widget.cursorHeight,
701 702
        cursorRadius: cursorRadius,
        cursorColor: cursorColor,
703 704
        selectionHeightStyle: widget.selectionHeightStyle,
        selectionWidthStyle: widget.selectionWidthStyle,
705 706 707 708 709
        cursorOpacityAnimates: cursorOpacityAnimates,
        cursorOffset: cursorOffset,
        paintCursorAboveText: paintCursorAboveText,
        backgroundCursorColor: CupertinoColors.inactiveGray,
        enableInteractiveSelection: widget.enableInteractiveSelection,
710
        magnifierConfiguration: widget.magnifierConfiguration ?? TextMagnifier.adaptiveMagnifierConfiguration,
711 712
        dragStartBehavior: widget.dragStartBehavior,
        scrollPhysics: widget.scrollPhysics,
713
        autofillHints: null,
714
        contextMenuBuilder: widget.contextMenuBuilder,
715 716 717 718
      ),
    );

    return Semantics(
719
      label: widget.semanticsLabel,
720
      excludeSemantics: widget.semanticsLabel != null,
721 722 723 724 725 726 727 728 729 730
      onLongPress: () {
        _effectiveFocusNode.requestFocus();
      },
      child: _selectionGestureDetectorBuilder.buildGestureDetector(
        behavior: HitTestBehavior.translucent,
        child: child,
      ),
    );
  }
}