diff --git a/lib/common/widgets/flutter/text/paragraph.dart b/lib/common/widgets/flutter/text/paragraph.dart deleted file mode 100644 index 4743ddede..000000000 --- a/lib/common/widgets/flutter/text/paragraph.dart +++ /dev/null @@ -1,3860 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// ignore_for_file: prefer_initializing_formals, uri_does_not_exist_in_doc_import - -/// @docImport 'package:flutter/widgets.dart'; -/// -/// @docImport 'editable.dart'; -library; - -import 'dart:math' as math; -import 'dart:ui' - as ui - show - BoxHeightStyle, - BoxWidthStyle, - Gradient, - LineMetrics, - Shader, - TextBox, - TextHeightBehavior; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart' hide RenderParagraph; -import 'package:flutter/services.dart'; - -/// The start and end positions for a text boundary. -typedef _TextBoundaryRecord = ({ - TextPosition boundaryStart, - TextPosition boundaryEnd, -}); - -/// Signature for a function that determines the [_TextBoundaryRecord] at the given -/// [TextPosition]. -typedef _TextBoundaryAtPosition = - _TextBoundaryRecord Function(TextPosition position); - -/// Signature for a function that determines the [_TextBoundaryRecord] at the given -/// [TextPosition], for the given [String]. -typedef _TextBoundaryAtPositionInText = - _TextBoundaryRecord Function(TextPosition position, String text); - -const String _kEllipsis = '\u2026'; - -class _UnspecifiedTextScaler extends TextScaler { - const _UnspecifiedTextScaler(); - - @override - Never get textScaleFactor => throw UnimplementedError(); - - @override - Never scale(double fontSize) => throw UnimplementedError(); -} - -/// A render object that displays a paragraph of text. -class RenderParagraph extends RenderBox - with - ContainerRenderObjectMixin, - RenderInlineChildrenContainerDefaults, - RelayoutWhenSystemFontsChangeMixin { - /// Creates a paragraph render object. - /// - /// The [maxLines] property may be null (and indeed defaults to null), but if - /// it is not null, it must be greater than zero. - RenderParagraph( - InlineSpan text, { - TextAlign textAlign = TextAlign.start, - required TextDirection textDirection, - bool softWrap = true, - TextOverflow overflow = TextOverflow.clip, - @Deprecated( - 'Use textScaler instead. ' - 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' - 'This feature was deprecated after v3.12.0-2.0.pre.', - ) - double textScaleFactor = 1.0, - TextScaler textScaler = const _UnspecifiedTextScaler(), - int? maxLines, - Locale? locale, - StrutStyle? strutStyle, - TextWidthBasis textWidthBasis = TextWidthBasis.parent, - ui.TextHeightBehavior? textHeightBehavior, - List? children, - Color? selectionColor, - SelectionRegistrar? registrar, - required Color primary, - VoidCallback? onShowMore, - }) : assert(text.debugAssertIsValid()), - assert(maxLines == null || maxLines > 0), - assert( - identical(textScaler, const _UnspecifiedTextScaler()) || - textScaleFactor == 1.0, - 'textScaleFactor is deprecated and cannot be specified when textScaler is specified.', - ), - _primary = primary, - _onShowMore = onShowMore, - _softWrap = softWrap, - _overflow = overflow, - _selectionColor = selectionColor, - _textPainter = TextPainter( - text: text, - textAlign: textAlign, - textDirection: textDirection, - textScaler: textScaler == const _UnspecifiedTextScaler() - ? TextScaler.linear(textScaleFactor) - : textScaler, - maxLines: maxLines, - ellipsis: overflow == TextOverflow.ellipsis ? _kEllipsis : null, - locale: locale, - strutStyle: strutStyle, - textWidthBasis: textWidthBasis, - textHeightBehavior: textHeightBehavior, - ) { - addAll(children); - this.registrar = registrar; - } - - static final String _placeholderCharacter = String.fromCharCode( - PlaceholderSpan.placeholderCodeUnit, - ); - - final TextPainter _textPainter; - - // Currently, computing min/max intrinsic width/height will destroy state - // inside the painter. Instead of calling _layout again to get back the correct - // state, use a separate TextPainter for intrinsics calculation. - // - // TODO(abarth): Make computing the min/max intrinsic width/height a - // non-destructive operation. - TextPainter? _textIntrinsicsCache; - - TextPainter get _textIntrinsics { - return (_textIntrinsicsCache ??= TextPainter()) - ..text = _textPainter.text - ..textAlign = _textPainter.textAlign - ..textDirection = _textPainter.textDirection - ..textScaler = _textPainter.textScaler - ..maxLines = _textPainter.maxLines - ..ellipsis = _textPainter.ellipsis - ..locale = _textPainter.locale - ..strutStyle = _textPainter.strutStyle - ..textWidthBasis = _textPainter.textWidthBasis - ..textHeightBehavior = _textPainter.textHeightBehavior; - } - - List? _cachedAttributedLabels; - - List? _cachedCombinedSemanticsInfos; - - /// The text to display. - InlineSpan get text => _textPainter.text!; - - set text(({InlineSpan text, Color primary}) params) { - final value = params.text; - _primary = params.primary; - if (_morePainter case final textPainter?) { - final textSpan = _moreTextSpan(value.style); - switch (textPainter.text!.compareTo(textSpan)) { - case RenderComparison.paint: - textPainter.text = textSpan; - case RenderComparison.layout: - textPainter - ..text = textSpan - ..layout(); - default: - } - } - - switch (_textPainter.text!.compareTo(value)) { - case RenderComparison.identical: - return; - case RenderComparison.metadata: - _textPainter.text = value; - _cachedCombinedSemanticsInfos = null; - markNeedsSemanticsUpdate(); - case RenderComparison.paint: - _textPainter.text = value; - _cachedAttributedLabels = null; - _cachedCombinedSemanticsInfos = null; - markNeedsPaint(); - markNeedsSemanticsUpdate(); - case RenderComparison.layout: - _textPainter.text = value; - _overflowShader = null; - _cachedAttributedLabels = null; - _cachedCombinedSemanticsInfos = null; - markNeedsLayout(); - _removeSelectionRegistrarSubscription(); - _disposeSelectableFragments(); - _updateSelectionRegistrarSubscription(); - } - } - - /// The ongoing selections in this paragraph. - /// - /// The selection does not include selections in [PlaceholderSpan] if there - /// are any. - @visibleForTesting - List get selections { - if (_lastSelectableFragments == null) { - return const []; - } - final List results = []; - for (final _SelectableFragment fragment in _lastSelectableFragments!) { - if (fragment._textSelectionStart != null && - fragment._textSelectionEnd != null) { - results.add( - TextSelection( - baseOffset: fragment._textSelectionStart!.offset, - extentOffset: fragment._textSelectionEnd!.offset, - ), - ); - } - } - return results; - } - - // Should be null if selection is not enabled, i.e. _registrar = null. The - // paragraph splits on [PlaceholderSpan.placeholderCodeUnit], and stores each - // fragment in this list. - List<_SelectableFragment>? _lastSelectableFragments; - - /// The [SelectionRegistrar] this paragraph will be, or is, registered to. - SelectionRegistrar? get registrar => _registrar; - SelectionRegistrar? _registrar; - - set registrar(SelectionRegistrar? value) { - if (value == _registrar) { - return; - } - _removeSelectionRegistrarSubscription(); - _disposeSelectableFragments(); - _registrar = value; - _updateSelectionRegistrarSubscription(); - } - - void _updateSelectionRegistrarSubscription() { - if (_registrar == null) { - return; - } - _lastSelectableFragments ??= _getSelectableFragments(); - _lastSelectableFragments!.forEach(_registrar!.add); - if (_lastSelectableFragments!.isNotEmpty) { - markNeedsCompositingBitsUpdate(); - } - } - - void _removeSelectionRegistrarSubscription() { - if (_registrar == null || _lastSelectableFragments == null) { - return; - } - _lastSelectableFragments!.forEach(_registrar!.remove); - } - - List<_SelectableFragment> _getSelectableFragments() { - final String plainText = text.toPlainText(includeSemanticsLabels: false); - final List<_SelectableFragment> result = <_SelectableFragment>[]; - int start = 0; - while (start < plainText.length) { - int end = plainText.indexOf(_placeholderCharacter, start); - if (start != end) { - if (end == -1) { - end = plainText.length; - } - result.add( - _SelectableFragment( - paragraph: this, - range: TextRange(start: start, end: end), - fullText: plainText, - ), - ); - start = end; - } - start += 1; - } - return result; - } - - /// Determines whether the given [Selectable] was created by this - /// [RenderParagraph]. - /// - /// The [RenderParagraph] splits its text into multiple [Selectable]s, - /// delimited by [PlaceholderSpan]s or [WidgetSpan]s. - bool selectableBelongsToParagraph(Selectable selectable) { - if (_lastSelectableFragments == null) { - return false; - } - return _lastSelectableFragments!.contains(selectable); - } - - void _disposeSelectableFragments() { - if (_lastSelectableFragments == null) { - return; - } - for (final _SelectableFragment fragment in _lastSelectableFragments!) { - fragment.dispose(); - } - _lastSelectableFragments = null; - } - - @override - bool get alwaysNeedsCompositing => - _lastSelectableFragments?.isNotEmpty ?? false; - - @override - void markNeedsLayout() { - _lastSelectableFragments?.forEach( - (_SelectableFragment element) => element.didChangeParagraphLayout(), - ); - super.markNeedsLayout(); - } - - @override - void dispose() { - _removeSelectionRegistrarSubscription(); - _disposeSelectableFragments(); - _textPainter.dispose(); - _textIntrinsicsCache?.dispose(); - _tapGestureRecognizer?.dispose(); - _tapGestureRecognizer = null; - _morePainter?.dispose(); - _morePainter = null; - super.dispose(); - } - - /// How the text should be aligned horizontally. - TextAlign get textAlign => _textPainter.textAlign; - - set textAlign(TextAlign value) { - if (_textPainter.textAlign == value) { - return; - } - _textPainter.textAlign = value; - markNeedsPaint(); - } - - /// The directionality of the text. - /// - /// This decides how the [TextAlign.start], [TextAlign.end], and - /// [TextAlign.justify] values of [textAlign] are interpreted. - /// - /// This is also used to disambiguate how to render bidirectional text. For - /// example, if the [text] is an English phrase followed by a Hebrew phrase, - /// in a [TextDirection.ltr] context the English phrase will be on the left - /// and the Hebrew phrase to its right, while in a [TextDirection.rtl] - /// context, the English phrase will be on the right and the Hebrew phrase on - /// its left. - TextDirection get textDirection => _textPainter.textDirection!; - - set textDirection(TextDirection value) { - if (_textPainter.textDirection == value) { - return; - } - _textPainter.textDirection = value; - markNeedsLayout(); - } - - /// Whether the text should break at soft line breaks. - /// - /// If false, the glyphs in the text will be positioned as if there was - /// unlimited horizontal space. - /// - /// If [softWrap] is false, [overflow] and [textAlign] may have unexpected - /// effects. - bool get softWrap => _softWrap; - bool _softWrap; - - set softWrap(bool value) { - if (_softWrap == value) { - return; - } - _softWrap = value; - markNeedsLayout(); - } - - /// How visual overflow should be handled. - TextOverflow get overflow => _overflow; - TextOverflow _overflow; - - set overflow(TextOverflow value) { - if (_overflow == value) { - return; - } - _overflow = value; - _textPainter.ellipsis = value == TextOverflow.ellipsis ? _kEllipsis : null; - markNeedsLayout(); - } - - /// Deprecated. Will be removed in a future version of Flutter. Use - /// [textScaler] instead. - /// - /// The number of font pixels for each logical pixel. - /// - /// For example, if the text scale factor is 1.5, text will be 50% larger than - /// the specified font size. - @Deprecated( - 'Use textScaler instead. ' - 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' - 'This feature was deprecated after v3.12.0-2.0.pre.', - ) - double get textScaleFactor => _textPainter.textScaleFactor; - - @Deprecated( - 'Use textScaler instead. ' - 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' - 'This feature was deprecated after v3.12.0-2.0.pre.', - ) - set textScaleFactor(double value) { - textScaler = TextScaler.linear(value); - } - - /// {@macro flutter.painting.textPainter.textScaler} - TextScaler get textScaler => _textPainter.textScaler; - - set textScaler(TextScaler value) { - if (_textPainter.textScaler == value) { - return; - } - _morePainter - ?..textScaler = value - ..layout(); - _textPainter.textScaler = value; - _overflowShader = null; - markNeedsLayout(); - } - - /// An optional maximum number of lines for the text to span, wrapping if - /// necessary. If the text exceeds the given number of lines, it will be - /// truncated according to [overflow] and [softWrap]. - int? get maxLines => _textPainter.maxLines; - - /// The value may be null. If it is not null, then it must be greater than - /// zero. - set maxLines(int? value) { - assert(value == null || value > 0); - if (_textPainter.maxLines == value) { - return; - } - _textPainter.maxLines = value; - _overflowShader = null; - markNeedsLayout(); - } - - /// Used by this paragraph's internal [TextPainter] to select a - /// locale-specific font. - /// - /// In some cases, the same Unicode character may be rendered differently - /// depending on the locale. For example, the '骨' character is rendered - /// differently in the Chinese and Japanese locales. In these cases, the - /// [locale] may be used to select a locale-specific font. - Locale? get locale => _textPainter.locale; - - /// The value may be null. - set locale(Locale? value) { - if (_textPainter.locale == value) { - return; - } - _textPainter.locale = value; - _overflowShader = null; - markNeedsLayout(); - } - - /// {@macro flutter.painting.textPainter.strutStyle} - StrutStyle? get strutStyle => _textPainter.strutStyle; - - /// The value may be null. - set strutStyle(StrutStyle? value) { - if (_textPainter.strutStyle == value) { - return; - } - _textPainter.strutStyle = value; - _overflowShader = null; - markNeedsLayout(); - } - - /// {@macro flutter.painting.textPainter.textWidthBasis} - TextWidthBasis get textWidthBasis => _textPainter.textWidthBasis; - - set textWidthBasis(TextWidthBasis value) { - if (_textPainter.textWidthBasis == value) { - return; - } - _textPainter.textWidthBasis = value; - _overflowShader = null; - markNeedsLayout(); - } - - /// {@macro dart.ui.textHeightBehavior} - ui.TextHeightBehavior? get textHeightBehavior => - _textPainter.textHeightBehavior; - - set textHeightBehavior(ui.TextHeightBehavior? value) { - if (_textPainter.textHeightBehavior == value) { - return; - } - _textPainter.textHeightBehavior = value; - _overflowShader = null; - markNeedsLayout(); - } - - /// The color to use when painting the selection. - /// - /// Ignored if the text is not selectable (e.g. if [registrar] is null). - Color? get selectionColor => _selectionColor; - Color? _selectionColor; - - set selectionColor(Color? value) { - if (_selectionColor == value) { - return; - } - _selectionColor = value; - if (_lastSelectableFragments?.any( - (_SelectableFragment fragment) => fragment.value.hasSelection, - ) ?? - false) { - markNeedsPaint(); - } - } - - Offset _getOffsetForPosition(TextPosition position) { - return getOffsetForCaret(position, Rect.zero) + - Offset(0, getFullHeightForCaret(position)); - } - - @override - double computeMinIntrinsicWidth(double height) { - final List placeholderDimensions = - layoutInlineChildren( - double.infinity, - (RenderBox child, BoxConstraints constraints) => - Size(child.getMinIntrinsicWidth(double.infinity), 0.0), - ChildLayoutHelper.getDryBaseline, - ); - return (_textIntrinsics - ..setPlaceholderDimensions(placeholderDimensions) - ..layout()) - .minIntrinsicWidth; - } - - @override - double computeMaxIntrinsicWidth(double height) { - final List - placeholderDimensions = layoutInlineChildren( - double.infinity, - // Height and baseline is irrelevant as all text will be laid - // out in a single line. Therefore, using 0.0 as a dummy for the height. - (RenderBox child, BoxConstraints constraints) => - Size(child.getMaxIntrinsicWidth(double.infinity), 0.0), - ChildLayoutHelper.getDryBaseline, - ); - return (_textIntrinsics - ..setPlaceholderDimensions(placeholderDimensions) - ..layout()) - .maxIntrinsicWidth; - } - - /// An estimate of the height of a line in the text. See [TextPainter.preferredLineHeight]. - /// - /// This does not require the layout to be updated. - @visibleForTesting - double get preferredLineHeight => _textPainter.preferredLineHeight; - - double _computeIntrinsicHeight(double width) { - return (_textIntrinsics - ..setPlaceholderDimensions( - layoutInlineChildren( - width, - ChildLayoutHelper.dryLayoutChild, - ChildLayoutHelper.getDryBaseline, - ), - ) - ..layout(minWidth: width, maxWidth: _adjustMaxWidth(width))) - .height; - } - - @override - double computeMinIntrinsicHeight(double width) { - return _computeIntrinsicHeight(width); - } - - @override - double computeMaxIntrinsicHeight(double width) { - return _computeIntrinsicHeight(width); - } - - @override - bool hitTestSelf(Offset position) => true; - - @override - @protected - bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { - if (_tapGestureRecognizer != null) { - if (_morePainter case final textPainter?) { - late final height = _textPainter.height; - if (position.dx < textPainter.width && - position.dy > height && - position.dy < height + textPainter.height) { - result.add(HitTestEntry(textPainter.text as TextSpan)); - return true; - } - } - } - - final GlyphInfo? glyph = _textPainter.getClosestGlyphForOffset(position); - // The hit-test can't fall through the horizontal gaps between visually - // adjacent characters on the same line, even with a large letter-spacing or - // text justification, as graphemeClusterLayoutBounds.width is the advance - // width to the next character, so there's no gap between their - // graphemeClusterLayoutBounds rects. - final InlineSpan? spanHit = - glyph != null && glyph.graphemeClusterLayoutBounds.contains(position) - ? _textPainter.text!.getSpanForPosition( - TextPosition(offset: glyph.graphemeClusterCodeUnitRange.start), - ) - : null; - switch (spanHit) { - case final HitTestTarget span: - result.add(HitTestEntry(span)); - return true; - case _: - return hitTestInlineChildren(result, position); - } - } - - bool _needsClipping = false; - ui.Shader? _overflowShader; - - /// Whether this paragraph currently has a [dart:ui.Shader] for its overflow - /// effect. - /// - /// Used to test this object. Not for use in production. - @visibleForTesting - bool get debugHasOverflowShader => _overflowShader != null; - - @override - void systemFontsDidChange() { - super.systemFontsDidChange(); - _textPainter.markNeedsLayout(); - } - - // Placeholder dimensions representing the sizes of child inline widgets. - // - // These need to be cached because the text painter's placeholder dimensions - // will be overwritten during intrinsic width/height calculations and must be - // restored to the original values before final layout and painting. - List? _placeholderDimensions; - - double _adjustMaxWidth(double maxWidth) { - return softWrap || overflow == TextOverflow.ellipsis - ? maxWidth - : double.infinity; - } - - void _layoutTextWithConstraints(BoxConstraints constraints) { - _textPainter - ..setPlaceholderDimensions(_placeholderDimensions) - ..layout( - minWidth: constraints.minWidth, - maxWidth: _adjustMaxWidth(constraints.maxWidth), - ); - } - - @override - @protected - Size computeDryLayout(covariant BoxConstraints constraints) { - final Size size = - (_textIntrinsics - ..setPlaceholderDimensions( - layoutInlineChildren( - constraints.maxWidth, - ChildLayoutHelper.dryLayoutChild, - ChildLayoutHelper.getDryBaseline, - ), - ) - ..layout( - minWidth: constraints.minWidth, - maxWidth: _adjustMaxWidth(constraints.maxWidth), - )) - .size; - return constraints.constrain(size); - } - - @override - double computeDistanceToActualBaseline(TextBaseline baseline) { - assert(!debugNeedsLayout); - assert(constraints.debugAssertIsValid()); - _layoutTextWithConstraints(constraints); - // TODO(garyq): Since our metric for ideographic baseline is currently - // inaccurate and the non-alphabetic baselines are based off of the - // alphabetic baseline, we use the alphabetic for now to produce correct - // layouts. We should eventually change this back to pass the `baseline` - // property when the ideographic baseline is properly implemented - // (https://github.com/flutter/flutter/issues/22625). - return _textPainter.computeDistanceToActualBaseline( - TextBaseline.alphabetic, - ); - } - - @override - double computeDryBaseline( - covariant BoxConstraints constraints, - TextBaseline baseline, - ) { - assert(constraints.debugAssertIsValid()); - _textIntrinsics - ..setPlaceholderDimensions( - layoutInlineChildren( - constraints.maxWidth, - ChildLayoutHelper.dryLayoutChild, - ChildLayoutHelper.getDryBaseline, - ), - ) - ..layout( - minWidth: constraints.minWidth, - maxWidth: _adjustMaxWidth(constraints.maxWidth), - ); - return _textIntrinsics.computeDistanceToActualBaseline( - TextBaseline.alphabetic, - ); - } - - Color _primary; - - VoidCallback? _onShowMore; - set onShowMore(VoidCallback? onShowMore) { - if (_onShowMore != onShowMore) { - _onShowMore = onShowMore; - _tapGestureRecognizer?.onTap = onShowMore; - } - } - - TapGestureRecognizer? _tapGestureRecognizer; - - TextSpan _moreTextSpan([TextStyle? style]) => TextSpan( - style: (style ?? text.style!).copyWith(color: _primary), - text: '查看更多', - recognizer: _tapGestureRecognizer, - ); - TextPainter? _morePainter; - - bool didOverflowHeight = false; - - @override - void performLayout() { - _lastSelectableFragments?.forEach( - (_SelectableFragment element) => element.didChangeParagraphLayout(), - ); - final BoxConstraints constraints = this.constraints; - _placeholderDimensions = layoutInlineChildren( - constraints.maxWidth, - ChildLayoutHelper.layoutChild, - ChildLayoutHelper.getBaseline, - ); - _layoutTextWithConstraints(constraints); - positionInlineChildren(_textPainter.inlinePlaceholderBoxes!); - - final Size textSize = _textPainter.size; - size = constraints.constrain(textSize); - - didOverflowHeight = - size.height < textSize.height || _textPainter.didExceedMaxLines; - - if (didOverflowHeight) { - if (_onShowMore != null) { - _tapGestureRecognizer ??= TapGestureRecognizer()..onTap = _onShowMore; - } - _morePainter ??= TextPainter( - text: _moreTextSpan(), - textDirection: textDirection, - textScaler: textScaler, - locale: locale, - )..layout(maxWidth: constraints.maxWidth); - size = Size( - size.width, - constraints.constrainHeight(size.height + _morePainter!.height), - ); - } - - final bool didOverflowWidth = size.width < textSize.width; - // TODO(abarth): We're only measuring the sizes of the line boxes here. If - // the glyphs draw outside the line boxes, we might think that there isn't - // visual overflow when there actually is visual overflow. This can become - // a problem if we start having horizontal overflow and introduce a clip - // that affects the actual (but undetected) vertical overflow. - final bool hasVisualOverflow = didOverflowWidth || didOverflowHeight; - if (hasVisualOverflow) { - switch (_overflow) { - case TextOverflow.visible: - _needsClipping = false; - _overflowShader = null; - case TextOverflow.clip: - case TextOverflow.ellipsis: - _needsClipping = true; - _overflowShader = null; - case TextOverflow.fade: - _needsClipping = true; - final TextPainter fadeSizePainter = TextPainter( - text: TextSpan(style: _textPainter.text!.style, text: '\u2026'), - textDirection: textDirection, - textScaler: textScaler, - locale: locale, - )..layout(); - if (didOverflowWidth) { - final (double fadeStart, double fadeEnd) = switch (textDirection) { - TextDirection.rtl => (fadeSizePainter.width, 0.0), - TextDirection.ltr => ( - size.width - fadeSizePainter.width, - size.width, - ), - }; - _overflowShader = ui.Gradient.linear( - Offset(fadeStart, 0.0), - Offset(fadeEnd, 0.0), - [const Color(0xFFFFFFFF), const Color(0x00FFFFFF)], - ); - } else { - final double fadeEnd = size.height; - final double fadeStart = fadeEnd - fadeSizePainter.height / 2.0; - _overflowShader = ui.Gradient.linear( - Offset(0.0, fadeStart), - Offset(0.0, fadeEnd), - [const Color(0xFFFFFFFF), const Color(0x00FFFFFF)], - ); - } - fadeSizePainter.dispose(); - } - } else { - _needsClipping = false; - _overflowShader = null; - } - } - - @override - void applyPaintTransform(RenderBox child, Matrix4 transform) { - defaultApplyPaintTransform(child, transform); - } - - @override - void paint(PaintingContext context, Offset offset) { - // Text alignment only triggers repaint so it's possible the text layout has - // been invalidated but performLayout wasn't called at this point. Make sure - // the TextPainter has a valid layout. - _layoutTextWithConstraints(constraints); - assert(() { - if (debugRepaintTextRainbowEnabled) { - final Paint paint = Paint()..color = debugCurrentRepaintColor.toColor(); - context.canvas.drawRect(offset & size, paint); - } - return true; - }()); - - if (_needsClipping) { - final Rect bounds = offset & size; - if (_overflowShader != null) { - // This layer limits what the shader below blends with to be just the - // text (as opposed to the text and its background). - context.canvas.saveLayer(bounds, Paint()); - } else { - context.canvas.save(); - } - context.canvas.clipRect(bounds); - } - - if (_lastSelectableFragments != null) { - for (final _SelectableFragment fragment in _lastSelectableFragments!) { - fragment.paint(context, offset); - } - } - - assert(() { - _textPainter.debugPaintTextLayoutBoxes = debugPaintTextLayoutBoxes; - return true; - }()); - - _textPainter.paint(context.canvas, offset); - - paintInlineChildren(context, offset); - - if (_needsClipping) { - if (_overflowShader != null) { - context.canvas.translate(offset.dx, offset.dy); - final Paint paint = Paint() - ..blendMode = BlendMode.modulate - ..shader = _overflowShader; - context.canvas.drawRect(Offset.zero & size, paint); - } - context.canvas.restore(); - } - - if (didOverflowHeight) { - _morePainter?.paint( - context.canvas, - offset + Offset(0, _textPainter.height), - ); - } - } - - /// Returns the offset at which to paint the caret. - /// - /// Valid only after [layout]. - Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) { - assert(!debugNeedsLayout); - _layoutTextWithConstraints(constraints); - return _textPainter.getOffsetForCaret(position, caretPrototype); - } - - /// {@macro flutter.painting.textPainter.getFullHeightForCaret} - /// - /// Valid only after [layout]. - double getFullHeightForCaret(TextPosition position) { - assert(!debugNeedsLayout); - _layoutTextWithConstraints(constraints); - return _textPainter.getFullHeightForCaret(position, Rect.zero); - } - - /// Returns a list of rects that bound the given selection. - /// - /// The [boxHeightStyle] and [boxWidthStyle] arguments may be used to select - /// the shape of the [TextBox]es. These properties default to - /// [ui.BoxHeightStyle.tight] and [ui.BoxWidthStyle.tight] respectively. - /// - /// A given selection might have more than one rect if the [RenderParagraph] - /// contains multiple [InlineSpan]s or bidirectional text, because logically - /// contiguous text might not be visually contiguous. - /// - /// Valid only after [layout]. - /// - /// See also: - /// - /// * [TextPainter.getBoxesForSelection], the method in TextPainter to get - /// the equivalent boxes. - List getBoxesForSelection( - TextSelection selection, { - ui.BoxHeightStyle boxHeightStyle = ui.BoxHeightStyle.tight, - ui.BoxWidthStyle boxWidthStyle = ui.BoxWidthStyle.tight, - }) { - assert(!debugNeedsLayout); - _layoutTextWithConstraints(constraints); - return _textPainter.getBoxesForSelection( - selection, - boxHeightStyle: boxHeightStyle, - boxWidthStyle: boxWidthStyle, - ); - } - - /// Returns the position within the text for the given pixel offset. - /// - /// Valid only after [layout]. - TextPosition getPositionForOffset(Offset offset) { - assert(!debugNeedsLayout); - _layoutTextWithConstraints(constraints); - return _textPainter.getPositionForOffset(offset); - } - - /// Returns the text range of the word at the given offset. Characters not - /// part of a word, such as spaces, symbols, and punctuation, have word breaks - /// on both sides. In such cases, this method will return a text range that - /// contains the given text position. - /// - /// Word boundaries are defined more precisely in Unicode Standard Annex #29 - /// . - /// - /// Valid only after [layout]. - TextRange getWordBoundary(TextPosition position) { - assert(!debugNeedsLayout); - _layoutTextWithConstraints(constraints); - return _textPainter.getWordBoundary(position); - } - - TextRange _getLineAtOffset(TextPosition position) => - _textPainter.getLineBoundary(position); - - TextPosition _getTextPositionAbove(TextPosition position) { - // -0.5 of preferredLineHeight points to the middle of the line above. - final double preferredLineHeight = _textPainter.preferredLineHeight; - final double verticalOffset = -0.5 * preferredLineHeight; - return _getTextPositionVertical(position, verticalOffset); - } - - TextPosition _getTextPositionBelow(TextPosition position) { - // 1.5 of preferredLineHeight points to the middle of the line below. - final double preferredLineHeight = _textPainter.preferredLineHeight; - final double verticalOffset = 1.5 * preferredLineHeight; - return _getTextPositionVertical(position, verticalOffset); - } - - TextPosition _getTextPositionVertical( - TextPosition position, - double verticalOffset, - ) { - final Offset caretOffset = _textPainter.getOffsetForCaret( - position, - Rect.zero, - ); - final Offset caretOffsetTranslated = caretOffset.translate( - 0.0, - verticalOffset, - ); - return _textPainter.getPositionForOffset(caretOffsetTranslated); - } - - /// Returns the size of the text as laid out. - /// - /// This can differ from [size] if the text overflowed or if the [constraints] - /// provided by the parent [RenderObject] forced the layout to be bigger than - /// necessary for the given [text]. - /// - /// This returns the [TextPainter.size] of the underlying [TextPainter]. - /// - /// Valid only after [layout]. - Size get textSize { - assert(!debugNeedsLayout); - return _textPainter.size; - } - - /// Whether the text was truncated or ellipsized as laid out. - /// - /// This returns the [TextPainter.didExceedMaxLines] of the underlying [TextPainter]. - /// - /// Valid only after [layout]. - bool get didExceedMaxLines { - assert(!debugNeedsLayout); - return _textPainter.didExceedMaxLines; - } - - /// Collected during [describeSemanticsConfiguration], used by - /// [assembleSemanticsNode]. - List? _semanticsInfo; - - @override - void describeSemanticsConfiguration(SemanticsConfiguration config) { - super.describeSemanticsConfiguration(config); - _semanticsInfo = text.getSemanticsInformation(); - bool needsAssembleSemanticsNode = false; - bool needsChildConfigurationsDelegate = false; - for (final InlineSpanSemanticsInformation info in _semanticsInfo!) { - if (info.recognizer != null || info.semanticsIdentifier != null) { - needsAssembleSemanticsNode = true; - break; - } - needsChildConfigurationsDelegate = - needsChildConfigurationsDelegate || info.isPlaceholder; - } - - if (needsAssembleSemanticsNode) { - config - ..explicitChildNodes = true - ..isSemanticBoundary = true; - } else if (needsChildConfigurationsDelegate) { - config.childConfigurationsDelegate = - _childSemanticsConfigurationsDelegate; - } else { - if (_cachedAttributedLabels == null) { - final StringBuffer buffer = StringBuffer(); - int offset = 0; - final List attributes = []; - for (final InlineSpanSemanticsInformation info in _semanticsInfo!) { - final String label = info.semanticsLabel ?? info.text; - for (final StringAttribute infoAttribute in info.stringAttributes) { - final TextRange originalRange = infoAttribute.range; - attributes.add( - infoAttribute.copy( - range: TextRange( - start: offset + originalRange.start, - end: offset + originalRange.end, - ), - ), - ); - } - buffer.write(label); - offset += label.length; - } - _cachedAttributedLabels = [ - AttributedString(buffer.toString(), attributes: attributes), - ]; - } - config - ..attributedLabel = _cachedAttributedLabels![0] - ..textDirection = textDirection; - } - } - - ChildSemanticsConfigurationsResult _childSemanticsConfigurationsDelegate( - List childConfigs, - ) { - final ChildSemanticsConfigurationsResultBuilder builder = - ChildSemanticsConfigurationsResultBuilder(); - int placeholderIndex = 0; - int childConfigsIndex = 0; - int attributedLabelCacheIndex = 0; - InlineSpanSemanticsInformation? seenTextInfo; - _cachedCombinedSemanticsInfos ??= combineSemanticsInfo(_semanticsInfo!); - for (final InlineSpanSemanticsInformation info - in _cachedCombinedSemanticsInfos!) { - if (info.isPlaceholder) { - if (seenTextInfo != null) { - builder.markAsMergeUp( - _createSemanticsConfigForTextInfo( - seenTextInfo, - attributedLabelCacheIndex, - ), - ); - attributedLabelCacheIndex += 1; - } - // Mark every childConfig belongs to this placeholder to merge up group. - while (childConfigsIndex < childConfigs.length && - childConfigs[childConfigsIndex].tagsChildrenWith( - PlaceholderSpanIndexSemanticsTag(placeholderIndex), - )) { - builder.markAsMergeUp(childConfigs[childConfigsIndex]); - childConfigsIndex += 1; - } - placeholderIndex += 1; - } else { - seenTextInfo = info; - } - } - - // Handle plain text info at the end. - if (seenTextInfo != null) { - builder.markAsMergeUp( - _createSemanticsConfigForTextInfo( - seenTextInfo, - attributedLabelCacheIndex, - ), - ); - } - return builder.build(); - } - - SemanticsConfiguration _createSemanticsConfigForTextInfo( - InlineSpanSemanticsInformation textInfo, - int cacheIndex, - ) { - assert(!textInfo.requiresOwnNode); - final List cachedStrings = _cachedAttributedLabels ??= - []; - assert(cacheIndex <= cachedStrings.length); - final bool hasCache = cacheIndex < cachedStrings.length; - - late AttributedString attributedLabel; - if (hasCache) { - attributedLabel = cachedStrings[cacheIndex]; - } else { - assert(cachedStrings.length == cacheIndex); - attributedLabel = AttributedString( - textInfo.semanticsLabel ?? textInfo.text, - attributes: textInfo.stringAttributes, - ); - cachedStrings.add(attributedLabel); - } - return SemanticsConfiguration() - ..textDirection = textDirection - ..attributedLabel = attributedLabel; - } - - // Caches [SemanticsNode]s created during [assembleSemanticsNode] so they - // can be re-used when [assembleSemanticsNode] is called again. This ensures - // stable ids for the [SemanticsNode]s of [TextSpan]s across - // [assembleSemanticsNode] invocations. - Map? _cachedChildNodes; - - @override - void assembleSemanticsNode( - SemanticsNode node, - SemanticsConfiguration config, - Iterable children, - ) { - assert(_semanticsInfo != null && _semanticsInfo!.isNotEmpty); - final List newChildren = []; - TextDirection currentDirection = textDirection; - Rect currentRect; - double ordinal = 0.0; - int start = 0; - int placeholderIndex = 0; - int childIndex = 0; - RenderBox? child = firstChild; - final Map newChildCache = {}; - _cachedCombinedSemanticsInfos ??= combineSemanticsInfo(_semanticsInfo!); - for (final InlineSpanSemanticsInformation info - in _cachedCombinedSemanticsInfos!) { - final TextSelection selection = TextSelection( - baseOffset: start, - extentOffset: start + info.text.length, - ); - start += info.text.length; - - if (info.isPlaceholder) { - // A placeholder span may have 0 to multiple semantics nodes, we need - // to annotate all of the semantics nodes belong to this span. - while (children.length > childIndex && - children - .elementAt(childIndex) - .isTagged(PlaceholderSpanIndexSemanticsTag(placeholderIndex))) { - final SemanticsNode childNode = children.elementAt(childIndex); - final TextParentData parentData = - child!.parentData! as TextParentData; - // parentData.scale may be null if the render object is truncated. - if (parentData.offset != null) { - newChildren.add(childNode); - } - childIndex += 1; - } - child = childAfter(child!); - placeholderIndex += 1; - } else { - final TextDirection initialDirection = currentDirection; - final List rects = getBoxesForSelection(selection); - if (rects.isEmpty) { - continue; - } - Rect rect = rects.first.toRect(); - currentDirection = rects.first.direction; - for (final ui.TextBox textBox in rects.skip(1)) { - rect = rect.expandToInclude(textBox.toRect()); - currentDirection = textBox.direction; - } - // Any of the text boxes may have had infinite dimensions. - // We shouldn't pass infinite dimensions up to the bridges. - rect = Rect.fromLTWH( - math.max(0.0, rect.left), - math.max(0.0, rect.top), - math.min(rect.width, constraints.maxWidth), - math.min(rect.height, constraints.maxHeight), - ); - // round the current rectangle to make this API testable and add some - // padding so that the accessibility rects do not overlap with the text. - currentRect = Rect.fromLTRB( - rect.left.floorToDouble() - 4.0, - rect.top.floorToDouble() - 4.0, - rect.right.ceilToDouble() + 4.0, - rect.bottom.ceilToDouble() + 4.0, - ); - final SemanticsConfiguration configuration = SemanticsConfiguration() - ..sortKey = OrdinalSortKey(ordinal++) - ..textDirection = initialDirection - ..identifier = info.semanticsIdentifier ?? '' - ..attributedLabel = AttributedString( - info.semanticsLabel ?? info.text, - attributes: info.stringAttributes, - ); - switch (info.recognizer) { - case TapGestureRecognizer(onTap: final VoidCallback? handler): - case DoubleTapGestureRecognizer( - onDoubleTap: final VoidCallback? handler, - ): - if (handler != null) { - configuration - ..onTap = handler - ..isLink = true; - } - case LongPressGestureRecognizer( - onLongPress: final GestureLongPressCallback? onLongPress, - ): - if (onLongPress != null) { - configuration.onLongPress = onLongPress; - } - case null: - break; - default: - assert(false, '${info.recognizer.runtimeType} is not supported.'); - } - if (node.parentPaintClipRect != null) { - final Rect paintRect = node.parentPaintClipRect!.intersect( - currentRect, - ); - configuration.isHidden = paintRect.isEmpty && !currentRect.isEmpty; - } - final SemanticsNode newChild; - if (_cachedChildNodes?.isNotEmpty ?? false) { - newChild = _cachedChildNodes!.remove(_cachedChildNodes!.keys.first)!; - } else { - final UniqueKey key = UniqueKey(); - newChild = SemanticsNode( - key: key, - showOnScreen: _createShowOnScreenFor(key), - ); - } - newChild - ..updateWith(config: configuration) - ..rect = currentRect; - newChildCache[newChild.key!] = newChild; - newChildren.add(newChild); - } - } - // Makes sure we annotated all of the semantics children. - assert(childIndex == children.length); - assert(child == null); - - _cachedChildNodes = newChildCache; - node.updateWith(config: config, childrenInInversePaintOrder: newChildren); - } - - VoidCallback? _createShowOnScreenFor(Key key) { - return () { - final SemanticsNode node = _cachedChildNodes![key]!; - showOnScreen(descendant: this, rect: node.rect); - }; - } - - @override - void clearSemantics() { - super.clearSemantics(); - _cachedChildNodes = null; - } - - @override - List debugDescribeChildren() { - return [ - text.toDiagnosticsNode( - name: 'text', - style: DiagnosticsTreeStyle.transition, - ), - ]; - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(EnumProperty('textAlign', textAlign)) - ..add(EnumProperty('textDirection', textDirection)) - ..add( - FlagProperty( - 'softWrap', - value: softWrap, - ifTrue: 'wrapping at box width', - ifFalse: 'no wrapping except at line break characters', - showName: true, - ), - ) - ..add(EnumProperty('overflow', overflow)) - ..add( - DiagnosticsProperty( - 'textScaler', - textScaler, - defaultValue: TextScaler.noScaling, - ), - ) - ..add( - DiagnosticsProperty('locale', locale, defaultValue: null), - ) - ..add(IntProperty('maxLines', maxLines, ifNull: 'unlimited')); - } -} - -/// A continuous, selectable piece of paragraph. -/// -/// Since the selections in [PlaceholderSpan] are handled independently in its -/// subtree, a selection in [RenderParagraph] can't continue across a -/// [PlaceholderSpan]. The [RenderParagraph] splits itself on [PlaceholderSpan] -/// to create multiple `_SelectableFragment`s so that they can be selected -/// separately. -class _SelectableFragment - with Selectable, Diagnosticable, ChangeNotifier - implements TextLayoutMetrics { - _SelectableFragment({ - required this.paragraph, - required this.fullText, - required this.range, - }) : assert(range.isValid && !range.isCollapsed && range.isNormalized) { - if (kFlutterMemoryAllocationsEnabled) { - ChangeNotifier.maybeDispatchObjectCreation(this); - } - _selectionGeometry = _getSelectionGeometry(); - } - - final TextRange range; - final RenderParagraph paragraph; - final String fullText; - - TextPosition? _textSelectionStart; - TextPosition? _textSelectionEnd; - - bool _selectableContainsOriginTextBoundary = false; - - LayerLink? _startHandleLayerLink; - LayerLink? _endHandleLayerLink; - - @override - SelectionGeometry get value => _selectionGeometry; - late SelectionGeometry _selectionGeometry; - - void _updateSelectionGeometry() { - final SelectionGeometry newValue = _getSelectionGeometry(); - - if (_selectionGeometry == newValue) { - return; - } - _selectionGeometry = newValue; - notifyListeners(); - } - - SelectionGeometry _getSelectionGeometry() { - if (_textSelectionStart == null || _textSelectionEnd == null) { - return const SelectionGeometry( - status: SelectionStatus.none, - hasContent: true, - ); - } - - final int selectionStart = _textSelectionStart!.offset; - final int selectionEnd = _textSelectionEnd!.offset; - final bool isReversed = selectionStart > selectionEnd; - final Offset startOffsetInParagraphCoordinates = paragraph - ._getOffsetForPosition( - TextPosition(offset: selectionStart), - ); - final Offset endOffsetInParagraphCoordinates = - selectionStart == selectionEnd - ? startOffsetInParagraphCoordinates - : paragraph._getOffsetForPosition(TextPosition(offset: selectionEnd)); - final bool flipHandles = - isReversed != (TextDirection.rtl == paragraph.textDirection); - final TextSelection selection = TextSelection( - baseOffset: selectionStart, - extentOffset: selectionEnd, - ); - final List selectionRects = []; - for (final TextBox textBox in paragraph.getBoxesForSelection(selection)) { - selectionRects.add(textBox.toRect()); - } - final bool selectionCollapsed = selectionStart == selectionEnd; - final ( - TextSelectionHandleType startSelectionHandleType, - TextSelectionHandleType endSelectionHandleType, - ) = switch ((selectionCollapsed, flipHandles)) { - // Always prefer collapsed handle when selection is collapsed. - (true, _) => ( - TextSelectionHandleType.collapsed, - TextSelectionHandleType.collapsed, - ), - (false, true) => ( - TextSelectionHandleType.right, - TextSelectionHandleType.left, - ), - (false, false) => ( - TextSelectionHandleType.left, - TextSelectionHandleType.right, - ), - }; - return SelectionGeometry( - startSelectionPoint: SelectionPoint( - localPosition: startOffsetInParagraphCoordinates, - lineHeight: paragraph._textPainter.preferredLineHeight, - handleType: startSelectionHandleType, - ), - endSelectionPoint: SelectionPoint( - localPosition: endOffsetInParagraphCoordinates, - lineHeight: paragraph._textPainter.preferredLineHeight, - handleType: endSelectionHandleType, - ), - selectionRects: selectionRects, - status: selectionCollapsed - ? SelectionStatus.collapsed - : SelectionStatus.uncollapsed, - hasContent: true, - ); - } - - @override - SelectionResult dispatchSelectionEvent(SelectionEvent event) { - late final SelectionResult result; - final TextPosition? existingSelectionStart = _textSelectionStart; - final TextPosition? existingSelectionEnd = _textSelectionEnd; - switch (event.type) { - case SelectionEventType.startEdgeUpdate: - case SelectionEventType.endEdgeUpdate: - final SelectionEdgeUpdateEvent edgeUpdate = - event as SelectionEdgeUpdateEvent; - final TextGranularity granularity = event.granularity; - - switch (granularity) { - case TextGranularity.character: - result = _updateSelectionEdge( - edgeUpdate.globalPosition, - isEnd: edgeUpdate.type == SelectionEventType.endEdgeUpdate, - ); - case TextGranularity.word: - result = _updateSelectionEdgeByTextBoundary( - edgeUpdate.globalPosition, - isEnd: edgeUpdate.type == SelectionEventType.endEdgeUpdate, - getTextBoundary: _getWordBoundaryAtPosition, - ); - case TextGranularity.paragraph: - result = _updateSelectionEdgeByMultiSelectableTextBoundary( - edgeUpdate.globalPosition, - isEnd: edgeUpdate.type == SelectionEventType.endEdgeUpdate, - getTextBoundary: _getParagraphBoundaryAtPosition, - getClampedTextBoundary: _getClampedParagraphBoundaryAtPosition, - ); - case TextGranularity.document: - case TextGranularity.line: - assert( - false, - 'Moving the selection edge by line or document is not supported.', - ); - } - case SelectionEventType.clear: - result = _handleClearSelection(); - case SelectionEventType.selectAll: - result = _handleSelectAll(); - case SelectionEventType.selectWord: - final SelectWordSelectionEvent selectWord = - event as SelectWordSelectionEvent; - result = _handleSelectWord(selectWord.globalPosition); - case SelectionEventType.selectParagraph: - final SelectParagraphSelectionEvent selectParagraph = - event as SelectParagraphSelectionEvent; - if (selectParagraph.absorb) { - _handleSelectAll(); - result = SelectionResult.next; - _selectableContainsOriginTextBoundary = true; - } else { - result = _handleSelectParagraph(selectParagraph.globalPosition); - } - case SelectionEventType.granularlyExtendSelection: - final GranularlyExtendSelectionEvent granularlyExtendSelection = - event as GranularlyExtendSelectionEvent; - result = _handleGranularlyExtendSelection( - granularlyExtendSelection.forward, - granularlyExtendSelection.isEnd, - granularlyExtendSelection.granularity, - ); - case SelectionEventType.directionallyExtendSelection: - final DirectionallyExtendSelectionEvent directionallyExtendSelection = - event as DirectionallyExtendSelectionEvent; - result = _handleDirectionallyExtendSelection( - directionallyExtendSelection.dx, - directionallyExtendSelection.isEnd, - directionallyExtendSelection.direction, - ); - } - - if (existingSelectionStart != _textSelectionStart || - existingSelectionEnd != _textSelectionEnd) { - _didChangeSelection(); - } - return result; - } - - @override - SelectedContent? getSelectedContent() { - if (_textSelectionStart == null || _textSelectionEnd == null) { - return null; - } - final int start = math.min( - _textSelectionStart!.offset, - _textSelectionEnd!.offset, - ); - final int end = math.max( - _textSelectionStart!.offset, - _textSelectionEnd!.offset, - ); - return SelectedContent(plainText: fullText.substring(start, end)); - } - - @override - SelectedContentRange? getSelection() { - if (_textSelectionStart == null || _textSelectionEnd == null) { - return null; - } - return SelectedContentRange( - startOffset: _textSelectionStart!.offset, - endOffset: _textSelectionEnd!.offset, - ); - } - - void _didChangeSelection() { - paragraph.markNeedsPaint(); - _updateSelectionGeometry(); - } - - TextPosition _updateSelectionStartEdgeByTextBoundary( - _TextBoundaryRecord? textBoundary, - _TextBoundaryAtPosition getTextBoundary, - TextPosition position, - TextPosition? existingSelectionStart, - TextPosition? existingSelectionEnd, - ) { - TextPosition? targetPosition; - if (textBoundary != null) { - assert( - textBoundary.boundaryStart.offset >= range.start && - textBoundary.boundaryEnd.offset <= range.end, - ); - if (_selectableContainsOriginTextBoundary && - existingSelectionStart != null && - existingSelectionEnd != null) { - final bool isSamePosition = - position.offset == existingSelectionEnd.offset; - final bool isSelectionInverted = - existingSelectionStart.offset > existingSelectionEnd.offset; - final bool shouldSwapEdges = - !isSamePosition && - (isSelectionInverted != - (position.offset > existingSelectionEnd.offset)); - if (shouldSwapEdges) { - if (position.offset < existingSelectionEnd.offset) { - targetPosition = textBoundary.boundaryStart; - } else { - targetPosition = textBoundary.boundaryEnd; - } - // When the selection is inverted by the new position it is necessary to - // swap the start edge (moving edge) with the end edge (static edge) to - // maintain the origin text boundary within the selection. - final _TextBoundaryRecord localTextBoundary = getTextBoundary( - existingSelectionEnd, - ); - assert( - localTextBoundary.boundaryStart.offset >= range.start && - localTextBoundary.boundaryEnd.offset <= range.end, - ); - _setSelectionPosition( - existingSelectionEnd.offset == - localTextBoundary.boundaryStart.offset - ? localTextBoundary.boundaryEnd - : localTextBoundary.boundaryStart, - isEnd: true, - ); - } else { - if (position.offset < existingSelectionEnd.offset) { - targetPosition = textBoundary.boundaryStart; - } else if (position.offset > existingSelectionEnd.offset) { - targetPosition = textBoundary.boundaryEnd; - } else { - // Keep the origin text boundary in bounds when position is at the static edge. - targetPosition = existingSelectionStart; - } - } - } else { - if (existingSelectionEnd != null) { - // If the end edge exists and the start edge is being moved, then the - // start edge is moved to encompass the entire text boundary at the new position. - if (position.offset < existingSelectionEnd.offset) { - targetPosition = textBoundary.boundaryStart; - } else { - targetPosition = textBoundary.boundaryEnd; - } - } else { - // Move the start edge to the closest text boundary. - targetPosition = _closestTextBoundary(textBoundary, position); - } - } - } else { - // The position is not contained within the current rect. The targetPosition - // will either be at the end or beginning of the current rect. See [SelectionUtils.adjustDragOffset] - // for a more in depth explanation on this adjustment. - if (_selectableContainsOriginTextBoundary && - existingSelectionStart != null && - existingSelectionEnd != null) { - // When the selection is inverted by the new position it is necessary to - // swap the start edge (moving edge) with the end edge (static edge) to - // maintain the origin text boundary within the selection. - final bool isSamePosition = - position.offset == existingSelectionEnd.offset; - final bool isSelectionInverted = - existingSelectionStart.offset > existingSelectionEnd.offset; - final bool shouldSwapEdges = - !isSamePosition && - (isSelectionInverted != - (position.offset > existingSelectionEnd.offset)); - - if (shouldSwapEdges) { - final _TextBoundaryRecord localTextBoundary = getTextBoundary( - existingSelectionEnd, - ); - assert( - localTextBoundary.boundaryStart.offset >= range.start && - localTextBoundary.boundaryEnd.offset <= range.end, - ); - _setSelectionPosition( - isSelectionInverted - ? localTextBoundary.boundaryEnd - : localTextBoundary.boundaryStart, - isEnd: true, - ); - } - } - } - return targetPosition ?? position; - } - - TextPosition _updateSelectionEndEdgeByTextBoundary( - _TextBoundaryRecord? textBoundary, - _TextBoundaryAtPosition getTextBoundary, - TextPosition position, - TextPosition? existingSelectionStart, - TextPosition? existingSelectionEnd, - ) { - TextPosition? targetPosition; - if (textBoundary != null) { - assert( - textBoundary.boundaryStart.offset >= range.start && - textBoundary.boundaryEnd.offset <= range.end, - ); - if (_selectableContainsOriginTextBoundary && - existingSelectionStart != null && - existingSelectionEnd != null) { - final bool isSamePosition = - position.offset == existingSelectionStart.offset; - final bool isSelectionInverted = - existingSelectionStart.offset > existingSelectionEnd.offset; - final bool shouldSwapEdges = - !isSamePosition && - (isSelectionInverted != - (position.offset < existingSelectionStart.offset)); - if (shouldSwapEdges) { - if (position.offset < existingSelectionStart.offset) { - targetPosition = textBoundary.boundaryStart; - } else { - targetPosition = textBoundary.boundaryEnd; - } - // When the selection is inverted by the new position it is necessary to - // swap the end edge (moving edge) with the start edge (static edge) to - // maintain the origin text boundary within the selection. - final _TextBoundaryRecord localTextBoundary = getTextBoundary( - existingSelectionStart, - ); - assert( - localTextBoundary.boundaryStart.offset >= range.start && - localTextBoundary.boundaryEnd.offset <= range.end, - ); - _setSelectionPosition( - existingSelectionStart.offset == - localTextBoundary.boundaryStart.offset - ? localTextBoundary.boundaryEnd - : localTextBoundary.boundaryStart, - isEnd: false, - ); - } else { - if (position.offset < existingSelectionStart.offset) { - targetPosition = textBoundary.boundaryStart; - } else if (position.offset > existingSelectionStart.offset) { - targetPosition = textBoundary.boundaryEnd; - } else { - // Keep the origin text boundary in bounds when position is at the static edge. - targetPosition = existingSelectionEnd; - } - } - } else { - if (existingSelectionStart != null) { - // If the start edge exists and the end edge is being moved, then the - // end edge is moved to encompass the entire text boundary at the new position. - if (position.offset < existingSelectionStart.offset) { - targetPosition = textBoundary.boundaryStart; - } else { - targetPosition = textBoundary.boundaryEnd; - } - } else { - // Move the end edge to the closest text boundary. - targetPosition = _closestTextBoundary(textBoundary, position); - } - } - } else { - // The position is not contained within the current rect. The targetPosition - // will either be at the end or beginning of the current rect. See [SelectionUtils.adjustDragOffset] - // for a more in depth explanation on this adjustment. - if (_selectableContainsOriginTextBoundary && - existingSelectionStart != null && - existingSelectionEnd != null) { - // When the selection is inverted by the new position it is necessary to - // swap the end edge (moving edge) with the start edge (static edge) to - // maintain the origin text boundary within the selection. - final bool isSamePosition = - position.offset == existingSelectionStart.offset; - final bool isSelectionInverted = - existingSelectionStart.offset > existingSelectionEnd.offset; - final bool shouldSwapEdges = - isSelectionInverted != - (position.offset < existingSelectionStart.offset) || - isSamePosition; - if (shouldSwapEdges) { - final _TextBoundaryRecord localTextBoundary = getTextBoundary( - existingSelectionStart, - ); - assert( - localTextBoundary.boundaryStart.offset >= range.start && - localTextBoundary.boundaryEnd.offset <= range.end, - ); - _setSelectionPosition( - isSelectionInverted - ? localTextBoundary.boundaryStart - : localTextBoundary.boundaryEnd, - isEnd: false, - ); - } - } - } - return targetPosition ?? position; - } - - SelectionResult _updateSelectionEdgeByTextBoundary( - Offset globalPosition, { - required bool isEnd, - required _TextBoundaryAtPosition getTextBoundary, - }) { - // When the start/end edges are swapped, i.e. the start is after the end, and - // the scrollable synthesizes an event for the opposite edge, this will potentially - // move the opposite edge outside of the origin text boundary and we are unable to recover. - final TextPosition? existingSelectionStart = _textSelectionStart; - final TextPosition? existingSelectionEnd = _textSelectionEnd; - - _setSelectionPosition(null, isEnd: isEnd); - final Matrix4 transform = paragraph.getTransformTo(null)..invert(); - final Offset localPosition = MatrixUtils.transformPoint( - transform, - globalPosition, - ); - if (_rect.isEmpty) { - return SelectionUtils.getResultBasedOnRect(_rect, localPosition); - } - final Offset adjustedOffset = SelectionUtils.adjustDragOffset( - _rect, - localPosition, - direction: paragraph.textDirection, - ); - - final TextPosition position = paragraph.getPositionForOffset( - adjustedOffset, - ); - // Check if the original local position is within the rect, if it is not then - // we do not need to look up the text boundary for that position. This is to - // maintain a selectables selection collapsed at 0 when the local position is - // not located inside its rect. - _TextBoundaryRecord? textBoundary = _rect.contains(localPosition) - ? getTextBoundary(position) - : null; - if (textBoundary != null && - (textBoundary.boundaryStart.offset < range.start && - textBoundary.boundaryEnd.offset <= range.start || - textBoundary.boundaryStart.offset >= range.end && - textBoundary.boundaryEnd.offset > range.end)) { - // When the position is located at a placeholder inside of the text, then we may compute - // a text boundary that does not belong to the current selectable fragment. In this case - // we should invalidate the text boundary so that it is not taken into account when - // computing the target position. - textBoundary = null; - } - final TextPosition targetPosition = _clampTextPosition( - isEnd - ? _updateSelectionEndEdgeByTextBoundary( - textBoundary, - getTextBoundary, - position, - existingSelectionStart, - existingSelectionEnd, - ) - : _updateSelectionStartEdgeByTextBoundary( - textBoundary, - getTextBoundary, - position, - existingSelectionStart, - existingSelectionEnd, - ), - ); - - _setSelectionPosition(targetPosition, isEnd: isEnd); - if (targetPosition.offset == range.end) { - return SelectionResult.next; - } - - if (targetPosition.offset == range.start) { - return SelectionResult.previous; - } - // TODO(chunhtai): The geometry information should not be used to determine - // selection result. This is a workaround to RenderParagraph, where it does - // not have a way to get accurate text length if its text is truncated due to - // layout constraint. - return SelectionUtils.getResultBasedOnRect(_rect, localPosition); - } - - SelectionResult _updateSelectionEdge( - Offset globalPosition, { - required bool isEnd, - }) { - _setSelectionPosition(null, isEnd: isEnd); - final Matrix4 transform = paragraph.getTransformTo(null)..invert(); - final Offset localPosition = MatrixUtils.transformPoint( - transform, - globalPosition, - ); - if (_rect.isEmpty) { - return SelectionUtils.getResultBasedOnRect(_rect, localPosition); - } - final Offset adjustedOffset = SelectionUtils.adjustDragOffset( - _rect, - localPosition, - direction: paragraph.textDirection, - ); - - final TextPosition position = _clampTextPosition( - paragraph.getPositionForOffset(adjustedOffset), - ); - _setSelectionPosition(position, isEnd: isEnd); - if (position.offset == range.end) { - return SelectionResult.next; - } - if (position.offset == range.start) { - return SelectionResult.previous; - } - // TODO(chunhtai): The geometry information should not be used to determine - // selection result. This is a workaround to RenderParagraph, where it does - // not have a way to get accurate text length if its text is truncated due to - // layout constraint. - return SelectionUtils.getResultBasedOnRect(_rect, localPosition); - } - - // This method handles updating the start edge by a text boundary that may - // not be contained within this selectable fragment. It is possible - // that a boundary spans multiple selectable fragments when the text contains - // [WidgetSpan]s. - // - // This method differs from [_updateSelectionStartEdgeByTextBoundary] in that - // to pivot offset used to swap selection edges and maintain the origin - // text boundary selected may be located outside of this selectable fragment. - // - // See [_updateSelectionEndEdgeByMultiSelectableTextBoundary] for the method - // that handles updating the end edge. - SelectionResult? _updateSelectionStartEdgeByMultiSelectableTextBoundary( - _TextBoundaryAtPositionInText getTextBoundary, - bool paragraphContainsPosition, - TextPosition position, - TextPosition? existingSelectionStart, - TextPosition? existingSelectionEnd, - ) { - const bool isEnd = false; - if (_selectableContainsOriginTextBoundary && - existingSelectionStart != null && - existingSelectionEnd != null) { - // If this selectable contains the origin boundary, maintain the existing - // selection. - final bool forwardSelection = - existingSelectionEnd.offset >= existingSelectionStart.offset; - if (paragraphContainsPosition) { - // When the position is within the root paragraph, swap the start and end - // edges when the selection is inverted. - final _TextBoundaryRecord boundaryAtPosition = getTextBoundary( - position, - fullText, - ); - // To accurately retrieve the origin text boundary when the selection - // is forward, use existingSelectionEnd.offset - 1. This is necessary - // because in a forwards selection, existingSelectionEnd marks the end - // of the origin text boundary. Using the unmodified offset incorrectly - // targets the subsequent text boundary. - final _TextBoundaryRecord originTextBoundary = getTextBoundary( - forwardSelection - ? TextPosition( - offset: existingSelectionEnd.offset - 1, - affinity: existingSelectionEnd.affinity, - ) - : existingSelectionEnd, - fullText, - ); - final TextPosition targetPosition; - final int pivotOffset = forwardSelection - ? originTextBoundary.boundaryEnd.offset - : originTextBoundary.boundaryStart.offset; - final bool shouldSwapEdges = - !forwardSelection != (position.offset > pivotOffset); - if (position.offset < pivotOffset) { - targetPosition = boundaryAtPosition.boundaryStart; - } else if (position.offset > pivotOffset) { - targetPosition = boundaryAtPosition.boundaryEnd; - } else { - // Keep the origin text boundary in bounds when position is at the static edge. - targetPosition = forwardSelection - ? existingSelectionStart - : existingSelectionEnd; - } - if (shouldSwapEdges) { - _setSelectionPosition( - _clampTextPosition( - forwardSelection - ? originTextBoundary.boundaryStart - : originTextBoundary.boundaryEnd, - ), - isEnd: true, - ); - } - _setSelectionPosition(_clampTextPosition(targetPosition), isEnd: isEnd); - final bool finalSelectionIsForward = - _textSelectionEnd!.offset >= _textSelectionStart!.offset; - if (boundaryAtPosition.boundaryStart.offset > range.end && - boundaryAtPosition.boundaryEnd.offset > range.end) { - return SelectionResult.next; - } - if (boundaryAtPosition.boundaryStart.offset < range.start && - boundaryAtPosition.boundaryEnd.offset < range.start) { - return SelectionResult.previous; - } - if (finalSelectionIsForward) { - if (boundaryAtPosition.boundaryStart.offset >= - originTextBoundary.boundaryStart.offset) { - return SelectionResult.end; - } - if (boundaryAtPosition.boundaryStart.offset < - originTextBoundary.boundaryStart.offset) { - return SelectionResult.previous; - } - } else { - if (boundaryAtPosition.boundaryEnd.offset <= - originTextBoundary.boundaryEnd.offset) { - return SelectionResult.end; - } - if (boundaryAtPosition.boundaryEnd.offset > - originTextBoundary.boundaryEnd.offset) { - return SelectionResult.next; - } - } - } else { - // When the drag position is not contained within the root paragraph, - // swap the edges when the selection changes direction. - final TextPosition clampedPosition = _clampTextPosition(position); - // To accurately retrieve the origin text boundary when the selection - // is forward, use existingSelectionEnd.offset - 1. This is necessary - // because in a forwards selection, existingSelectionEnd marks the end - // of the origin text boundary. Using the unmodified offset incorrectly - // targets the subsequent text boundary. - final _TextBoundaryRecord originTextBoundary = getTextBoundary( - forwardSelection - ? TextPosition( - offset: existingSelectionEnd.offset - 1, - affinity: existingSelectionEnd.affinity, - ) - : existingSelectionEnd, - fullText, - ); - if (forwardSelection && clampedPosition.offset == range.start) { - _setSelectionPosition(clampedPosition, isEnd: isEnd); - return SelectionResult.previous; - } - if (!forwardSelection && clampedPosition.offset == range.end) { - _setSelectionPosition(clampedPosition, isEnd: isEnd); - return SelectionResult.next; - } - if (forwardSelection && clampedPosition.offset == range.end) { - _setSelectionPosition( - _clampTextPosition(originTextBoundary.boundaryStart), - isEnd: true, - ); - _setSelectionPosition(clampedPosition, isEnd: isEnd); - return SelectionResult.next; - } - if (!forwardSelection && clampedPosition.offset == range.start) { - _setSelectionPosition( - _clampTextPosition(originTextBoundary.boundaryEnd), - isEnd: true, - ); - _setSelectionPosition(clampedPosition, isEnd: isEnd); - return SelectionResult.previous; - } - } - } else { - // A paragraph boundary may not be completely contained within this root - // selectable fragment. Keep searching until we find the end of the - // boundary. Do not search when the current drag position is on a placeholder - // to allow traversal to reach that placeholder. - final bool positionOnPlaceholder = - paragraph.getWordBoundary(position).textInside(fullText) == - _placeholderCharacter; - if (!paragraphContainsPosition || positionOnPlaceholder) { - return null; - } - if (existingSelectionEnd != null) { - final _TextBoundaryRecord boundaryAtPosition = getTextBoundary( - position, - fullText, - ); - final bool backwardSelection = - existingSelectionStart == null && - existingSelectionEnd.offset == range.start || - existingSelectionStart == existingSelectionEnd && - existingSelectionEnd.offset == range.start || - existingSelectionStart != null && - existingSelectionStart.offset > existingSelectionEnd.offset; - if (boundaryAtPosition.boundaryStart.offset < range.start && - boundaryAtPosition.boundaryEnd.offset < range.start) { - _setSelectionPosition( - TextPosition(offset: range.start), - isEnd: isEnd, - ); - return SelectionResult.previous; - } - if (boundaryAtPosition.boundaryStart.offset > range.end && - boundaryAtPosition.boundaryEnd.offset > range.end) { - _setSelectionPosition(TextPosition(offset: range.end), isEnd: isEnd); - return SelectionResult.next; - } - if (backwardSelection) { - if (boundaryAtPosition.boundaryEnd.offset <= range.end) { - _setSelectionPosition( - _clampTextPosition(boundaryAtPosition.boundaryEnd), - isEnd: isEnd, - ); - return SelectionResult.end; - } - if (boundaryAtPosition.boundaryEnd.offset > range.end) { - _setSelectionPosition( - TextPosition(offset: range.end), - isEnd: isEnd, - ); - return SelectionResult.next; - } - } else { - _setSelectionPosition( - _clampTextPosition(boundaryAtPosition.boundaryStart), - isEnd: isEnd, - ); - if (boundaryAtPosition.boundaryStart.offset < range.start) { - return SelectionResult.previous; - } - if (boundaryAtPosition.boundaryStart.offset >= range.start) { - return SelectionResult.end; - } - } - } - } - return null; - } - - // This method handles updating the end edge by a text boundary that may - // not be contained within this selectable fragment. It is possible - // that a boundary spans multiple selectable fragments when the text contains - // [WidgetSpan]s. - // - // This method differs from [_updateSelectionEndEdgeByTextBoundary] in that - // to pivot offset used to swap selection edges and maintain the origin - // text boundary selected may be located outside of this selectable fragment. - // - // See [_updateSelectionStartEdgeByMultiSelectableTextBoundary] for the method - // that handles updating the end edge. - SelectionResult? _updateSelectionEndEdgeByMultiSelectableTextBoundary( - _TextBoundaryAtPositionInText getTextBoundary, - bool paragraphContainsPosition, - TextPosition position, - TextPosition? existingSelectionStart, - TextPosition? existingSelectionEnd, - ) { - const bool isEnd = true; - if (_selectableContainsOriginTextBoundary && - existingSelectionStart != null && - existingSelectionEnd != null) { - // If this selectable contains the origin boundary, maintain the existing - // selection. - final bool forwardSelection = - existingSelectionEnd.offset >= existingSelectionStart.offset; - if (paragraphContainsPosition) { - // When the position is within the root paragraph, swap the start and end - // edges when the selection is inverted. - final _TextBoundaryRecord boundaryAtPosition = getTextBoundary( - position, - fullText, - ); - // To accurately retrieve the origin text boundary when the selection - // is backwards, use existingSelectionStart.offset - 1. This is necessary - // because in a backwards selection, existingSelectionStart marks the end - // of the origin text boundary. Using the unmodified offset incorrectly - // targets the subsequent text boundary. - final _TextBoundaryRecord originTextBoundary = getTextBoundary( - forwardSelection - ? existingSelectionStart - : TextPosition( - offset: existingSelectionStart.offset - 1, - affinity: existingSelectionStart.affinity, - ), - fullText, - ); - final TextPosition targetPosition; - final int pivotOffset = forwardSelection - ? originTextBoundary.boundaryStart.offset - : originTextBoundary.boundaryEnd.offset; - final bool shouldSwapEdges = - !forwardSelection != (position.offset < pivotOffset); - if (position.offset < pivotOffset) { - targetPosition = boundaryAtPosition.boundaryStart; - } else if (position.offset > pivotOffset) { - targetPosition = boundaryAtPosition.boundaryEnd; - } else { - // Keep the origin text boundary in bounds when position is at the static edge. - targetPosition = forwardSelection - ? existingSelectionEnd - : existingSelectionStart; - } - if (shouldSwapEdges) { - _setSelectionPosition( - _clampTextPosition( - forwardSelection - ? originTextBoundary.boundaryEnd - : originTextBoundary.boundaryStart, - ), - isEnd: false, - ); - } - _setSelectionPosition(_clampTextPosition(targetPosition), isEnd: isEnd); - final bool finalSelectionIsForward = - _textSelectionEnd!.offset >= _textSelectionStart!.offset; - if (boundaryAtPosition.boundaryStart.offset > range.end && - boundaryAtPosition.boundaryEnd.offset > range.end) { - return SelectionResult.next; - } - if (boundaryAtPosition.boundaryStart.offset < range.start && - boundaryAtPosition.boundaryEnd.offset < range.start) { - return SelectionResult.previous; - } - if (finalSelectionIsForward) { - if (boundaryAtPosition.boundaryEnd.offset <= - originTextBoundary.boundaryEnd.offset) { - return SelectionResult.end; - } - if (boundaryAtPosition.boundaryEnd.offset > - originTextBoundary.boundaryEnd.offset) { - return SelectionResult.next; - } - } else { - if (boundaryAtPosition.boundaryStart.offset >= - originTextBoundary.boundaryStart.offset) { - return SelectionResult.end; - } - if (boundaryAtPosition.boundaryStart.offset < - originTextBoundary.boundaryStart.offset) { - return SelectionResult.previous; - } - } - } else { - // When the drag position is not contained within the root paragraph, - // swap the edges when the selection changes direction. - final TextPosition clampedPosition = _clampTextPosition(position); - // To accurately retrieve the origin text boundary when the selection - // is backwards, use existingSelectionStart.offset - 1. This is necessary - // because in a backwards selection, existingSelectionStart marks the end - // of the origin text boundary. Using the unmodified offset incorrectly - // targets the subsequent text boundary. - final _TextBoundaryRecord originTextBoundary = getTextBoundary( - forwardSelection - ? existingSelectionStart - : TextPosition( - offset: existingSelectionStart.offset - 1, - affinity: existingSelectionStart.affinity, - ), - fullText, - ); - if (forwardSelection && clampedPosition.offset == range.start) { - _setSelectionPosition( - _clampTextPosition(originTextBoundary.boundaryEnd), - isEnd: false, - ); - _setSelectionPosition(clampedPosition, isEnd: isEnd); - return SelectionResult.previous; - } - if (!forwardSelection && clampedPosition.offset == range.end) { - _setSelectionPosition( - _clampTextPosition(originTextBoundary.boundaryStart), - isEnd: false, - ); - _setSelectionPosition(clampedPosition, isEnd: isEnd); - return SelectionResult.next; - } - if (forwardSelection && clampedPosition.offset == range.end) { - _setSelectionPosition(clampedPosition, isEnd: isEnd); - return SelectionResult.next; - } - if (!forwardSelection && clampedPosition.offset == range.start) { - _setSelectionPosition(clampedPosition, isEnd: isEnd); - return SelectionResult.previous; - } - } - } else { - // A paragraph boundary may not be completely contained within this root - // selectable fragment. Keep searching until we find the end of the - // boundary. Do not search when the current drag position is on a placeholder - // to allow traversal to reach that placeholder. - final bool positionOnPlaceholder = - paragraph.getWordBoundary(position).textInside(fullText) == - _placeholderCharacter; - if (!paragraphContainsPosition || positionOnPlaceholder) { - return null; - } - if (existingSelectionStart != null) { - final _TextBoundaryRecord boundaryAtPosition = getTextBoundary( - position, - fullText, - ); - final bool backwardSelection = - existingSelectionEnd == null && - existingSelectionStart.offset == range.end || - existingSelectionStart == existingSelectionEnd && - existingSelectionStart.offset == range.end || - existingSelectionEnd != null && - existingSelectionStart.offset > existingSelectionEnd.offset; - if (boundaryAtPosition.boundaryStart.offset < range.start && - boundaryAtPosition.boundaryEnd.offset < range.start) { - _setSelectionPosition( - TextPosition(offset: range.start), - isEnd: isEnd, - ); - return SelectionResult.previous; - } - if (boundaryAtPosition.boundaryStart.offset > range.end && - boundaryAtPosition.boundaryEnd.offset > range.end) { - _setSelectionPosition(TextPosition(offset: range.end), isEnd: isEnd); - return SelectionResult.next; - } - if (backwardSelection) { - _setSelectionPosition( - _clampTextPosition(boundaryAtPosition.boundaryStart), - isEnd: isEnd, - ); - if (boundaryAtPosition.boundaryStart.offset < range.start) { - return SelectionResult.previous; - } - if (boundaryAtPosition.boundaryStart.offset >= range.start) { - return SelectionResult.end; - } - } else { - if (boundaryAtPosition.boundaryEnd.offset <= range.end) { - _setSelectionPosition( - _clampTextPosition(boundaryAtPosition.boundaryEnd), - isEnd: isEnd, - ); - return SelectionResult.end; - } - if (boundaryAtPosition.boundaryEnd.offset > range.end) { - _setSelectionPosition( - TextPosition(offset: range.end), - isEnd: isEnd, - ); - return SelectionResult.next; - } - } - } - } - return null; - } - - // The placeholder character used by [RenderParagraph]. - static final String _placeholderCharacter = String.fromCharCode( - PlaceholderSpan.placeholderCodeUnit, - ); - static final int _placeholderLength = _placeholderCharacter.length; - - // This method handles updating the start edge by a text boundary that may - // not be contained within this selectable fragment. It is possible - // that a boundary spans multiple selectable fragments when the text contains - // [WidgetSpan]s. - // - // This method differs from [_updateSelectionStartEdgeByMultiSelectableBoundary] - // in that to maintain the origin text boundary selected at a placeholder, - // this selectable fragment must be aware of the [RenderParagraph] that closely - // encompasses the complete origin text boundary. - // - // See [_updateSelectionEndEdgeAtPlaceholderByMultiSelectableTextBoundary] for the method - // that handles updating the end edge. - SelectionResult? - _updateSelectionStartEdgeAtPlaceholderByMultiSelectableTextBoundary( - _TextBoundaryAtPositionInText getTextBoundary, - Offset globalPosition, - bool paragraphContainsPosition, - TextPosition position, - TextPosition? existingSelectionStart, - TextPosition? existingSelectionEnd, - ) { - const bool isEnd = false; - if (_selectableContainsOriginTextBoundary && - existingSelectionStart != null && - existingSelectionEnd != null) { - // If this selectable contains the origin boundary, maintain the existing - // selection. - final bool forwardSelection = - existingSelectionEnd.offset >= existingSelectionStart.offset; - final RenderParagraph originParagraph = _getOriginParagraph(); - final bool fragmentBelongsToOriginParagraph = - originParagraph == paragraph; - if (fragmentBelongsToOriginParagraph) { - return _updateSelectionStartEdgeByMultiSelectableTextBoundary( - getTextBoundary, - paragraphContainsPosition, - position, - existingSelectionStart, - existingSelectionEnd, - ); - } - final Matrix4 originTransform = originParagraph.getTransformTo(null) - ..invert(); - final Offset originParagraphLocalPosition = MatrixUtils.transformPoint( - originTransform, - globalPosition, - ); - final bool positionWithinOriginParagraph = originParagraph.paintBounds - .contains( - originParagraphLocalPosition, - ); - final TextPosition positionRelativeToOriginParagraph = originParagraph - .getPositionForOffset( - originParagraphLocalPosition, - ); - if (positionWithinOriginParagraph) { - // When the selection is inverted by the new position it is necessary to - // swap the start edge (moving edge) with the end edge (static edge) to - // maintain the origin text boundary within the selection. - final String originText = originParagraph.text.toPlainText( - includeSemanticsLabels: false, - ); - final _TextBoundaryRecord boundaryAtPosition = getTextBoundary( - positionRelativeToOriginParagraph, - originText, - ); - final _TextBoundaryRecord originTextBoundary = getTextBoundary( - _getPositionInParagraph(originParagraph), - originText, - ); - final TextPosition targetPosition; - final int pivotOffset = forwardSelection - ? originTextBoundary.boundaryEnd.offset - : originTextBoundary.boundaryStart.offset; - final bool shouldSwapEdges = - !forwardSelection != - (positionRelativeToOriginParagraph.offset > pivotOffset); - if (positionRelativeToOriginParagraph.offset < pivotOffset) { - targetPosition = boundaryAtPosition.boundaryStart; - } else if (positionRelativeToOriginParagraph.offset > pivotOffset) { - targetPosition = boundaryAtPosition.boundaryEnd; - } else { - // Keep the origin text boundary in bounds when position is at the static edge. - targetPosition = existingSelectionStart; - } - if (shouldSwapEdges) { - _setSelectionPosition(existingSelectionStart, isEnd: true); - } - _setSelectionPosition(_clampTextPosition(targetPosition), isEnd: isEnd); - final bool finalSelectionIsForward = - _textSelectionEnd!.offset >= _textSelectionStart!.offset; - final TextPosition originParagraphPlaceholderTextPosition = - _getPositionInParagraph( - originParagraph, - ); - final TextRange originParagraphPlaceholderRange = TextRange( - start: originParagraphPlaceholderTextPosition.offset, - end: - originParagraphPlaceholderTextPosition.offset + - _placeholderLength, - ); - if (boundaryAtPosition.boundaryStart.offset > - originParagraphPlaceholderRange.end && - boundaryAtPosition.boundaryEnd.offset > - originParagraphPlaceholderRange.end) { - return SelectionResult.next; - } - if (boundaryAtPosition.boundaryStart.offset < - originParagraphPlaceholderRange.start && - boundaryAtPosition.boundaryEnd.offset < - originParagraphPlaceholderRange.start) { - return SelectionResult.previous; - } - if (finalSelectionIsForward) { - if (boundaryAtPosition.boundaryEnd.offset <= - originTextBoundary.boundaryEnd.offset) { - return SelectionResult.end; - } - if (boundaryAtPosition.boundaryEnd.offset > - originTextBoundary.boundaryEnd.offset) { - return SelectionResult.next; - } - } else { - if (boundaryAtPosition.boundaryStart.offset >= - originTextBoundary.boundaryStart.offset) { - return SelectionResult.end; - } - if (boundaryAtPosition.boundaryStart.offset < - originTextBoundary.boundaryStart.offset) { - return SelectionResult.previous; - } - } - } else { - // When the drag position is not contained within the origin paragraph, - // swap the edges when the selection changes direction. - // - // [SelectionUtils.adjustDragOffset] will adjust the given [Offset] to the - // beginning or end of the provided [Rect] based on whether the [Offset] - // is located within the given [Rect]. - final Offset adjustedOffset = SelectionUtils.adjustDragOffset( - originParagraph.paintBounds, - originParagraphLocalPosition, - direction: paragraph.textDirection, - ); - final TextPosition adjustedPositionRelativeToOriginParagraph = - originParagraph.getPositionForOffset(adjustedOffset); - final TextPosition originParagraphPlaceholderTextPosition = - _getPositionInParagraph( - originParagraph, - ); - final TextRange originParagraphPlaceholderRange = TextRange( - start: originParagraphPlaceholderTextPosition.offset, - end: - originParagraphPlaceholderTextPosition.offset + - _placeholderLength, - ); - if (forwardSelection && - adjustedPositionRelativeToOriginParagraph.offset <= - originParagraphPlaceholderRange.start) { - _setSelectionPosition( - TextPosition(offset: range.start), - isEnd: isEnd, - ); - return SelectionResult.previous; - } - if (!forwardSelection && - adjustedPositionRelativeToOriginParagraph.offset >= - originParagraphPlaceholderRange.end) { - _setSelectionPosition(TextPosition(offset: range.end), isEnd: isEnd); - return SelectionResult.next; - } - if (forwardSelection && - adjustedPositionRelativeToOriginParagraph.offset >= - originParagraphPlaceholderRange.end) { - _setSelectionPosition(existingSelectionStart, isEnd: true); - _setSelectionPosition(TextPosition(offset: range.end), isEnd: isEnd); - return SelectionResult.next; - } - if (!forwardSelection && - adjustedPositionRelativeToOriginParagraph.offset <= - originParagraphPlaceholderRange.start) { - _setSelectionPosition(existingSelectionStart, isEnd: true); - _setSelectionPosition( - TextPosition(offset: range.start), - isEnd: isEnd, - ); - return SelectionResult.previous; - } - } - } else { - // When the drag position is somewhere on the root text and not a placeholder, - // traverse the selectable fragments relative to the [RenderParagraph] that - // contains the drag position. - if (paragraphContainsPosition) { - return _updateSelectionStartEdgeByMultiSelectableTextBoundary( - getTextBoundary, - paragraphContainsPosition, - position, - existingSelectionStart, - existingSelectionEnd, - ); - } - if (existingSelectionEnd != null) { - final ({RenderParagraph paragraph, Offset localPosition})? - targetDetails = _getParagraphContainingPosition(globalPosition); - if (targetDetails == null) { - return null; - } - final RenderParagraph targetParagraph = targetDetails.paragraph; - final TextPosition positionRelativeToTargetParagraph = targetParagraph - .getPositionForOffset( - targetDetails.localPosition, - ); - final String targetText = targetParagraph.text.toPlainText( - includeSemanticsLabels: false, - ); - final bool positionOnPlaceholder = - targetParagraph - .getWordBoundary(positionRelativeToTargetParagraph) - .textInside(targetText) == - _placeholderCharacter; - if (positionOnPlaceholder) { - return null; - } - final bool backwardSelection = - existingSelectionStart == null && - existingSelectionEnd.offset == range.start || - existingSelectionStart == existingSelectionEnd && - existingSelectionEnd.offset == range.start || - existingSelectionStart != null && - existingSelectionStart.offset > existingSelectionEnd.offset; - final _TextBoundaryRecord boundaryAtPositionRelativeToTargetParagraph = - getTextBoundary( - positionRelativeToTargetParagraph, - targetText, - ); - final TextPosition targetParagraphPlaceholderTextPosition = - _getPositionInParagraph( - targetParagraph, - ); - final TextRange targetParagraphPlaceholderRange = TextRange( - start: targetParagraphPlaceholderTextPosition.offset, - end: - targetParagraphPlaceholderTextPosition.offset + - _placeholderLength, - ); - if (boundaryAtPositionRelativeToTargetParagraph.boundaryStart.offset < - targetParagraphPlaceholderRange.start && - boundaryAtPositionRelativeToTargetParagraph.boundaryEnd.offset < - targetParagraphPlaceholderRange.start) { - _setSelectionPosition( - TextPosition(offset: range.start), - isEnd: isEnd, - ); - return SelectionResult.previous; - } - if (boundaryAtPositionRelativeToTargetParagraph.boundaryStart.offset > - targetParagraphPlaceholderRange.end && - boundaryAtPositionRelativeToTargetParagraph.boundaryEnd.offset > - targetParagraphPlaceholderRange.end) { - _setSelectionPosition(TextPosition(offset: range.end), isEnd: isEnd); - return SelectionResult.next; - } - if (backwardSelection) { - if (boundaryAtPositionRelativeToTargetParagraph.boundaryEnd.offset <= - targetParagraphPlaceholderRange.end) { - _setSelectionPosition( - TextPosition(offset: range.end), - isEnd: isEnd, - ); - return SelectionResult.end; - } - if (boundaryAtPositionRelativeToTargetParagraph.boundaryEnd.offset > - targetParagraphPlaceholderRange.end) { - _setSelectionPosition( - TextPosition(offset: range.end), - isEnd: isEnd, - ); - return SelectionResult.next; - } - } else { - if (boundaryAtPositionRelativeToTargetParagraph - .boundaryStart - .offset >= - targetParagraphPlaceholderRange.start) { - _setSelectionPosition( - TextPosition(offset: range.start), - isEnd: isEnd, - ); - return SelectionResult.end; - } - if (boundaryAtPositionRelativeToTargetParagraph.boundaryStart.offset < - targetParagraphPlaceholderRange.start) { - _setSelectionPosition( - TextPosition(offset: range.start), - isEnd: isEnd, - ); - return SelectionResult.previous; - } - } - } - } - return null; - } - - // This method handles updating the end edge by a text boundary that may - // not be contained within this selectable fragment. It is possible - // that a boundary spans multiple selectable fragments when the text contains - // [WidgetSpan]s. - // - // This method differs from [_updateSelectionEndEdgeByMultiSelectableBoundary] - // in that to maintain the origin text boundary selected at a placeholder, this - // selectable fragment must be aware of the [RenderParagraph] that closely - // encompasses the complete origin text boundary. - // - // See [_updateSelectionStartEdgeAtPlaceholderByMultiSelectableTextBoundary] - // for the method that handles updating the start edge. - SelectionResult? - _updateSelectionEndEdgeAtPlaceholderByMultiSelectableTextBoundary( - _TextBoundaryAtPositionInText getTextBoundary, - Offset globalPosition, - bool paragraphContainsPosition, - TextPosition position, - TextPosition? existingSelectionStart, - TextPosition? existingSelectionEnd, - ) { - const bool isEnd = true; - if (_selectableContainsOriginTextBoundary && - existingSelectionStart != null && - existingSelectionEnd != null) { - // If this selectable contains the origin boundary, maintain the existing - // selection. - final bool forwardSelection = - existingSelectionEnd.offset >= existingSelectionStart.offset; - final RenderParagraph originParagraph = _getOriginParagraph(); - final bool fragmentBelongsToOriginParagraph = - originParagraph == paragraph; - if (fragmentBelongsToOriginParagraph) { - return _updateSelectionEndEdgeByMultiSelectableTextBoundary( - getTextBoundary, - paragraphContainsPosition, - position, - existingSelectionStart, - existingSelectionEnd, - ); - } - final Matrix4 originTransform = originParagraph.getTransformTo(null) - ..invert(); - final Offset originParagraphLocalPosition = MatrixUtils.transformPoint( - originTransform, - globalPosition, - ); - final bool positionWithinOriginParagraph = originParagraph.paintBounds - .contains( - originParagraphLocalPosition, - ); - final TextPosition positionRelativeToOriginParagraph = originParagraph - .getPositionForOffset( - originParagraphLocalPosition, - ); - if (positionWithinOriginParagraph) { - // When the selection is inverted by the new position it is necessary to - // swap the end edge (moving edge) with the start edge (static edge) to - // maintain the origin text boundary within the selection. - final String originText = originParagraph.text.toPlainText( - includeSemanticsLabels: false, - ); - final _TextBoundaryRecord boundaryAtPosition = getTextBoundary( - positionRelativeToOriginParagraph, - originText, - ); - final _TextBoundaryRecord originTextBoundary = getTextBoundary( - _getPositionInParagraph(originParagraph), - originText, - ); - final TextPosition targetPosition; - final int pivotOffset = forwardSelection - ? originTextBoundary.boundaryStart.offset - : originTextBoundary.boundaryEnd.offset; - final bool shouldSwapEdges = - !forwardSelection != - (positionRelativeToOriginParagraph.offset < pivotOffset); - if (positionRelativeToOriginParagraph.offset < pivotOffset) { - targetPosition = boundaryAtPosition.boundaryStart; - } else if (positionRelativeToOriginParagraph.offset > pivotOffset) { - targetPosition = boundaryAtPosition.boundaryEnd; - } else { - // Keep the origin text boundary in bounds when position is at the static edge. - targetPosition = existingSelectionEnd; - } - if (shouldSwapEdges) { - _setSelectionPosition(existingSelectionEnd, isEnd: false); - } - _setSelectionPosition(_clampTextPosition(targetPosition), isEnd: isEnd); - final bool finalSelectionIsForward = - _textSelectionEnd!.offset >= _textSelectionStart!.offset; - final TextPosition originParagraphPlaceholderTextPosition = - _getPositionInParagraph( - originParagraph, - ); - final TextRange originParagraphPlaceholderRange = TextRange( - start: originParagraphPlaceholderTextPosition.offset, - end: - originParagraphPlaceholderTextPosition.offset + - _placeholderLength, - ); - if (boundaryAtPosition.boundaryStart.offset > - originParagraphPlaceholderRange.end && - boundaryAtPosition.boundaryEnd.offset > - originParagraphPlaceholderRange.end) { - return SelectionResult.next; - } - if (boundaryAtPosition.boundaryStart.offset < - originParagraphPlaceholderRange.start && - boundaryAtPosition.boundaryEnd.offset < - originParagraphPlaceholderRange.start) { - return SelectionResult.previous; - } - if (finalSelectionIsForward) { - if (boundaryAtPosition.boundaryEnd.offset <= - originTextBoundary.boundaryEnd.offset) { - return SelectionResult.end; - } - if (boundaryAtPosition.boundaryEnd.offset > - originTextBoundary.boundaryEnd.offset) { - return SelectionResult.next; - } - } else { - if (boundaryAtPosition.boundaryStart.offset >= - originTextBoundary.boundaryStart.offset) { - return SelectionResult.end; - } - if (boundaryAtPosition.boundaryStart.offset < - originTextBoundary.boundaryStart.offset) { - return SelectionResult.previous; - } - } - } else { - // When the drag position is not contained within the origin paragraph, - // swap the edges when the selection changes direction. - // - // [SelectionUtils.adjustDragOffset] will adjust the given [Offset] to the - // beginning or end of the provided [Rect] based on whether the [Offset] - // is located within the given [Rect]. - final Offset adjustedOffset = SelectionUtils.adjustDragOffset( - originParagraph.paintBounds, - originParagraphLocalPosition, - direction: paragraph.textDirection, - ); - final TextPosition adjustedPositionRelativeToOriginParagraph = - originParagraph.getPositionForOffset(adjustedOffset); - final TextPosition originParagraphPlaceholderTextPosition = - _getPositionInParagraph( - originParagraph, - ); - final TextRange originParagraphPlaceholderRange = TextRange( - start: originParagraphPlaceholderTextPosition.offset, - end: - originParagraphPlaceholderTextPosition.offset + - _placeholderLength, - ); - if (forwardSelection && - adjustedPositionRelativeToOriginParagraph.offset <= - originParagraphPlaceholderRange.start) { - _setSelectionPosition(existingSelectionEnd, isEnd: false); - _setSelectionPosition( - TextPosition(offset: range.start), - isEnd: isEnd, - ); - return SelectionResult.previous; - } - if (!forwardSelection && - adjustedPositionRelativeToOriginParagraph.offset >= - originParagraphPlaceholderRange.end) { - _setSelectionPosition(existingSelectionEnd, isEnd: false); - _setSelectionPosition(TextPosition(offset: range.end), isEnd: isEnd); - return SelectionResult.next; - } - if (forwardSelection && - adjustedPositionRelativeToOriginParagraph.offset >= - originParagraphPlaceholderRange.end) { - _setSelectionPosition(TextPosition(offset: range.end), isEnd: isEnd); - return SelectionResult.next; - } - if (!forwardSelection && - adjustedPositionRelativeToOriginParagraph.offset <= - originParagraphPlaceholderRange.start) { - _setSelectionPosition( - TextPosition(offset: range.start), - isEnd: isEnd, - ); - return SelectionResult.previous; - } - } - } else { - // When the drag position is somewhere on the root text and not a placeholder, - // traverse the selectable fragments relative to the [RenderParagraph] that - // contains the drag position. - if (paragraphContainsPosition) { - return _updateSelectionEndEdgeByMultiSelectableTextBoundary( - getTextBoundary, - paragraphContainsPosition, - position, - existingSelectionStart, - existingSelectionEnd, - ); - } - if (existingSelectionStart != null) { - final ({RenderParagraph paragraph, Offset localPosition})? - targetDetails = _getParagraphContainingPosition(globalPosition); - if (targetDetails == null) { - return null; - } - final RenderParagraph targetParagraph = targetDetails.paragraph; - final TextPosition positionRelativeToTargetParagraph = targetParagraph - .getPositionForOffset( - targetDetails.localPosition, - ); - final String targetText = targetParagraph.text.toPlainText( - includeSemanticsLabels: false, - ); - final bool positionOnPlaceholder = - targetParagraph - .getWordBoundary(positionRelativeToTargetParagraph) - .textInside(targetText) == - _placeholderCharacter; - if (positionOnPlaceholder) { - return null; - } - final bool backwardSelection = - existingSelectionEnd == null && - existingSelectionStart.offset == range.end || - existingSelectionStart == existingSelectionEnd && - existingSelectionStart.offset == range.end || - existingSelectionEnd != null && - existingSelectionStart.offset > existingSelectionEnd.offset; - final _TextBoundaryRecord boundaryAtPositionRelativeToTargetParagraph = - getTextBoundary( - positionRelativeToTargetParagraph, - targetText, - ); - final TextPosition targetParagraphPlaceholderTextPosition = - _getPositionInParagraph( - targetParagraph, - ); - final TextRange targetParagraphPlaceholderRange = TextRange( - start: targetParagraphPlaceholderTextPosition.offset, - end: - targetParagraphPlaceholderTextPosition.offset + - _placeholderLength, - ); - if (boundaryAtPositionRelativeToTargetParagraph.boundaryStart.offset < - targetParagraphPlaceholderRange.start && - boundaryAtPositionRelativeToTargetParagraph.boundaryEnd.offset < - targetParagraphPlaceholderRange.start) { - _setSelectionPosition( - TextPosition(offset: range.start), - isEnd: isEnd, - ); - return SelectionResult.previous; - } - if (boundaryAtPositionRelativeToTargetParagraph.boundaryStart.offset > - targetParagraphPlaceholderRange.end && - boundaryAtPositionRelativeToTargetParagraph.boundaryEnd.offset > - targetParagraphPlaceholderRange.end) { - _setSelectionPosition(TextPosition(offset: range.end), isEnd: isEnd); - return SelectionResult.next; - } - if (backwardSelection) { - if (boundaryAtPositionRelativeToTargetParagraph - .boundaryStart - .offset >= - targetParagraphPlaceholderRange.start) { - _setSelectionPosition( - TextPosition(offset: range.start), - isEnd: isEnd, - ); - return SelectionResult.end; - } - if (boundaryAtPositionRelativeToTargetParagraph.boundaryStart.offset < - targetParagraphPlaceholderRange.start) { - _setSelectionPosition( - TextPosition(offset: range.start), - isEnd: isEnd, - ); - return SelectionResult.previous; - } - } else { - if (boundaryAtPositionRelativeToTargetParagraph.boundaryEnd.offset <= - targetParagraphPlaceholderRange.end) { - _setSelectionPosition( - TextPosition(offset: range.end), - isEnd: isEnd, - ); - return SelectionResult.end; - } - if (boundaryAtPositionRelativeToTargetParagraph.boundaryEnd.offset > - targetParagraphPlaceholderRange.end) { - _setSelectionPosition( - TextPosition(offset: range.end), - isEnd: isEnd, - ); - return SelectionResult.next; - } - } - } - } - return null; - } - - SelectionResult _updateSelectionEdgeByMultiSelectableTextBoundary( - Offset globalPosition, { - required bool isEnd, - required _TextBoundaryAtPositionInText getTextBoundary, - required _TextBoundaryAtPosition getClampedTextBoundary, - }) { - // When the start/end edges are swapped, i.e. the start is after the end, and - // the scrollable synthesizes an event for the opposite edge, this will potentially - // move the opposite edge outside of the origin text boundary and we are unable to recover. - final TextPosition? existingSelectionStart = _textSelectionStart; - final TextPosition? existingSelectionEnd = _textSelectionEnd; - - _setSelectionPosition(null, isEnd: isEnd); - final Matrix4 transform = paragraph.getTransformTo(null); - transform.invert(); - final Offset localPosition = MatrixUtils.transformPoint( - transform, - globalPosition, - ); - if (_rect.isEmpty) { - return SelectionUtils.getResultBasedOnRect(_rect, localPosition); - } - final Offset adjustedOffset = SelectionUtils.adjustDragOffset( - _rect, - localPosition, - direction: paragraph.textDirection, - ); - final Offset adjustedOffsetRelativeToParagraph = - SelectionUtils.adjustDragOffset( - paragraph.paintBounds, - localPosition, - direction: paragraph.textDirection, - ); - - final TextPosition position = paragraph.getPositionForOffset( - adjustedOffset, - ); - final TextPosition positionInFullText = paragraph.getPositionForOffset( - adjustedOffsetRelativeToParagraph, - ); - - final SelectionResult? result; - if (_isPlaceholder()) { - result = isEnd - ? _updateSelectionEndEdgeAtPlaceholderByMultiSelectableTextBoundary( - getTextBoundary, - globalPosition, - paragraph.paintBounds.contains(localPosition), - positionInFullText, - existingSelectionStart, - existingSelectionEnd, - ) - : _updateSelectionStartEdgeAtPlaceholderByMultiSelectableTextBoundary( - getTextBoundary, - globalPosition, - paragraph.paintBounds.contains(localPosition), - positionInFullText, - existingSelectionStart, - existingSelectionEnd, - ); - } else { - result = isEnd - ? _updateSelectionEndEdgeByMultiSelectableTextBoundary( - getTextBoundary, - paragraph.paintBounds.contains(localPosition), - positionInFullText, - existingSelectionStart, - existingSelectionEnd, - ) - : _updateSelectionStartEdgeByMultiSelectableTextBoundary( - getTextBoundary, - paragraph.paintBounds.contains(localPosition), - positionInFullText, - existingSelectionStart, - existingSelectionEnd, - ); - } - if (result != null) { - return result; - } - - // Check if the original local position is within the rect, if it is not then - // we do not need to look up the text boundary for that position. This is to - // maintain a selectables selection collapsed at 0 when the local position is - // not located inside its rect. - _TextBoundaryRecord? textBoundary = _boundingBoxesContains(localPosition) - ? getClampedTextBoundary(position) - : null; - if (textBoundary != null && - (textBoundary.boundaryStart.offset < range.start && - textBoundary.boundaryEnd.offset <= range.start || - textBoundary.boundaryStart.offset >= range.end && - textBoundary.boundaryEnd.offset > range.end)) { - // When the position is located at a placeholder inside of the text, then we may compute - // a text boundary that does not belong to the current selectable fragment. In this case - // we should invalidate the text boundary so that it is not taken into account when - // computing the target position. - textBoundary = null; - } - final TextPosition targetPosition = _clampTextPosition( - isEnd - ? _updateSelectionEndEdgeByTextBoundary( - textBoundary, - getClampedTextBoundary, - position, - existingSelectionStart, - existingSelectionEnd, - ) - : _updateSelectionStartEdgeByTextBoundary( - textBoundary, - getClampedTextBoundary, - position, - existingSelectionStart, - existingSelectionEnd, - ), - ); - - _setSelectionPosition(targetPosition, isEnd: isEnd); - if (targetPosition.offset == range.end) { - return SelectionResult.next; - } - - if (targetPosition.offset == range.start) { - return SelectionResult.previous; - } - // TODO(chunhtai): The geometry information should not be used to determine - // selection result. This is a workaround to RenderParagraph, where it does - // not have a way to get accurate text length if its text is truncated due to - // layout constraint. - return SelectionUtils.getResultBasedOnRect(_rect, localPosition); - } - - TextPosition _closestTextBoundary( - _TextBoundaryRecord textBoundary, - TextPosition position, - ) { - final int differenceA = - (position.offset - textBoundary.boundaryStart.offset).abs(); - final int differenceB = (position.offset - textBoundary.boundaryEnd.offset) - .abs(); - return differenceA < differenceB - ? textBoundary.boundaryStart - : textBoundary.boundaryEnd; - } - - bool _isPlaceholder() { - // Determine whether this selectable fragment is a placeholder. - RenderObject? current = paragraph.parent; - while (current != null) { - if (current is RenderParagraph) { - return true; - } - current = current.parent; - } - return false; - } - - RenderParagraph _getOriginParagraph() { - // This method should only be called from a fragment that contains - // the origin boundary. By traversing up the RenderTree, determine the - // highest RenderParagraph that contains the origin text boundary. - assert(_selectableContainsOriginTextBoundary); - // Begin at the parent because it is guaranteed the paragraph containing - // this selectable fragment contains the origin boundary. - RenderObject? current = paragraph.parent; - RenderParagraph? originParagraph; - while (current != null) { - if (current is RenderParagraph) { - if (current._lastSelectableFragments != null) { - bool paragraphContainsOriginTextBoundary = false; - for (final _SelectableFragment fragment - in current._lastSelectableFragments!) { - if (fragment._selectableContainsOriginTextBoundary) { - paragraphContainsOriginTextBoundary = true; - originParagraph = current; - break; - } - } - if (!paragraphContainsOriginTextBoundary) { - return originParagraph ?? paragraph; - } - } - } - current = current.parent; - } - return originParagraph ?? paragraph; - } - - ({RenderParagraph paragraph, Offset localPosition})? - _getParagraphContainingPosition( - Offset globalPosition, - ) { - // This method will return the closest [RenderParagraph] whose rect - // contains the given `globalPosition` and the given `globalPosition` - // relative to that [RenderParagraph]. If no ancestor [RenderParagraph] - // contains the given `globalPosition` then this method will return null. - RenderObject? current = paragraph; - while (current != null) { - if (current is RenderParagraph) { - final Matrix4 currentTransform = current.getTransformTo(null)..invert(); - final Offset currentParagraphLocalPosition = MatrixUtils.transformPoint( - currentTransform, - globalPosition, - ); - final bool positionWithinCurrentParagraph = current.paintBounds - .contains( - currentParagraphLocalPosition, - ); - if (positionWithinCurrentParagraph) { - return ( - paragraph: current, - localPosition: currentParagraphLocalPosition, - ); - } - } - current = current.parent; - } - return null; - } - - bool _boundingBoxesContains(Offset position) { - for (final Rect rect in boundingBoxes) { - if (rect.contains(position)) { - return true; - } - } - return false; - } - - TextPosition _clampTextPosition(TextPosition position) { - // Affinity of range.end is upstream. - if (position.offset > range.end || - (position.offset == range.end && - position.affinity == TextAffinity.downstream)) { - return TextPosition(offset: range.end, affinity: TextAffinity.upstream); - } - if (position.offset < range.start) { - return TextPosition(offset: range.start); - } - return position; - } - - void _setSelectionPosition(TextPosition? position, {required bool isEnd}) { - if (isEnd) { - _textSelectionEnd = position; - } else { - _textSelectionStart = position; - } - } - - SelectionResult _handleClearSelection() { - _textSelectionStart = null; - _textSelectionEnd = null; - _selectableContainsOriginTextBoundary = false; - return SelectionResult.none; - } - - SelectionResult _handleSelectAll() { - _textSelectionStart = TextPosition(offset: range.start); - _textSelectionEnd = TextPosition( - offset: range.end, - affinity: TextAffinity.upstream, - ); - return SelectionResult.none; - } - - SelectionResult _handleSelectTextBoundary(_TextBoundaryRecord textBoundary) { - // This fragment may not contain the boundary, decide what direction the target - // fragment is located in. Because fragments are separated by placeholder - // spans, we also check if the beginning or end of the boundary is touching - // either edge of this fragment. - if (textBoundary.boundaryStart.offset < range.start && - textBoundary.boundaryEnd.offset <= range.start) { - return SelectionResult.previous; - } else if (textBoundary.boundaryStart.offset >= range.end && - textBoundary.boundaryEnd.offset > range.end) { - return SelectionResult.next; - } - // Fragments are separated by placeholder span, the text boundary shouldn't - // expand across fragments. - assert( - textBoundary.boundaryStart.offset >= range.start && - textBoundary.boundaryEnd.offset <= range.end, - ); - _textSelectionStart = textBoundary.boundaryStart; - _textSelectionEnd = textBoundary.boundaryEnd; - _selectableContainsOriginTextBoundary = true; - return SelectionResult.end; - } - - TextRange? _intersect(TextRange a, TextRange b) { - assert(a.isNormalized); - assert(b.isNormalized); - final int startMax = math.max(a.start, b.start); - final int endMin = math.min(a.end, b.end); - if (startMax <= endMin) { - // Intersection. - return TextRange(start: startMax, end: endMin); - } - return null; - } - - SelectionResult _handleSelectMultiFragmentTextBoundary( - _TextBoundaryRecord textBoundary, - ) { - // This fragment may not contain the boundary, decide what direction the target - // fragment is located in. Because fragments are separated by placeholder - // spans, we also check if the beginning or end of the boundary is touching - // either edge of this fragment. - if (textBoundary.boundaryStart.offset < range.start && - textBoundary.boundaryEnd.offset <= range.start) { - return SelectionResult.previous; - } else if (textBoundary.boundaryStart.offset >= range.end && - textBoundary.boundaryEnd.offset > range.end) { - return SelectionResult.next; - } - final TextRange boundaryAsRange = TextRange( - start: textBoundary.boundaryStart.offset, - end: textBoundary.boundaryEnd.offset, - ); - final TextRange? intersectRange = _intersect(range, boundaryAsRange); - if (intersectRange != null) { - _textSelectionStart = TextPosition(offset: intersectRange.start); - _textSelectionEnd = TextPosition(offset: intersectRange.end); - _selectableContainsOriginTextBoundary = true; - if (range.end < textBoundary.boundaryEnd.offset) { - return SelectionResult.next; - } - return SelectionResult.end; - } - return SelectionResult.none; - } - - _TextBoundaryRecord _adjustTextBoundaryAtPosition( - TextRange textBoundary, - TextPosition position, - ) { - late final TextPosition start; - late final TextPosition end; - if (position.offset > textBoundary.end) { - start = end = TextPosition(offset: position.offset); - } else { - start = TextPosition(offset: textBoundary.start); - end = TextPosition( - offset: textBoundary.end, - affinity: TextAffinity.upstream, - ); - } - return (boundaryStart: start, boundaryEnd: end); - } - - SelectionResult _handleSelectWord(Offset globalPosition) { - final TextPosition position = paragraph.getPositionForOffset( - paragraph.globalToLocal(globalPosition), - ); - if (_positionIsWithinCurrentSelection(position) && - _textSelectionStart != _textSelectionEnd) { - return SelectionResult.end; - } - final _TextBoundaryRecord wordBoundary = _getWordBoundaryAtPosition( - position, - ); - return _handleSelectTextBoundary(wordBoundary); - } - - _TextBoundaryRecord _getWordBoundaryAtPosition(TextPosition position) { - final TextRange word = paragraph.getWordBoundary(position); - assert(word.isNormalized); - return _adjustTextBoundaryAtPosition(word, position); - } - - SelectionResult _handleSelectParagraph(Offset globalPosition) { - final Offset localPosition = paragraph.globalToLocal(globalPosition); - final TextPosition position = paragraph.getPositionForOffset(localPosition); - final _TextBoundaryRecord paragraphBoundary = - _getParagraphBoundaryAtPosition( - position, - fullText, - ); - return _handleSelectMultiFragmentTextBoundary(paragraphBoundary); - } - - TextPosition _getPositionInParagraph(RenderParagraph targetParagraph) { - final Matrix4 transform = paragraph.getTransformTo(targetParagraph); - final Offset localCenter = paragraph.paintBounds.centerLeft; - final Offset localPos = MatrixUtils.transformPoint(transform, localCenter); - final TextPosition position = targetParagraph.getPositionForOffset( - localPos, - ); - return position; - } - - _TextBoundaryRecord _getParagraphBoundaryAtPosition( - TextPosition position, - String text, - ) { - final ParagraphBoundary paragraphBoundary = ParagraphBoundary(text); - // Use position.offset - 1 when `position` is at the end of the selectable to retrieve - // the previous text boundary's location. - final int paragraphStart = - paragraphBoundary.getLeadingTextBoundaryAt( - position.offset == text.length || - position.affinity == TextAffinity.upstream - ? position.offset - 1 - : position.offset, - ) ?? - 0; - final int paragraphEnd = - paragraphBoundary.getTrailingTextBoundaryAt(position.offset) ?? - text.length; - final TextRange paragraphRange = TextRange( - start: paragraphStart, - end: paragraphEnd, - ); - assert(paragraphRange.isNormalized); - return _adjustTextBoundaryAtPosition(paragraphRange, position); - } - - _TextBoundaryRecord _getClampedParagraphBoundaryAtPosition( - TextPosition position, - ) { - final ParagraphBoundary paragraphBoundary = ParagraphBoundary(fullText); - // Use position.offset - 1 when `position` is at the end of the selectable to retrieve - // the previous text boundary's location. - int paragraphStart = - paragraphBoundary.getLeadingTextBoundaryAt( - position.offset == fullText.length || - position.affinity == TextAffinity.upstream - ? position.offset - 1 - : position.offset, - ) ?? - 0; - int paragraphEnd = - paragraphBoundary.getTrailingTextBoundaryAt(position.offset) ?? - fullText.length; - paragraphStart = paragraphStart < range.start - ? range.start - : paragraphStart > range.end - ? range.end - : paragraphStart; - paragraphEnd = paragraphEnd > range.end - ? range.end - : paragraphEnd < range.start - ? range.start - : paragraphEnd; - final TextRange paragraphRange = TextRange( - start: paragraphStart, - end: paragraphEnd, - ); - assert(paragraphRange.isNormalized); - return _adjustTextBoundaryAtPosition(paragraphRange, position); - } - - SelectionResult _handleDirectionallyExtendSelection( - double horizontalBaseline, - bool isExtent, - SelectionExtendDirection movement, - ) { - final Matrix4 transform = paragraph.getTransformTo(null); - if (transform.invert() == 0.0) { - switch (movement) { - case SelectionExtendDirection.previousLine: - case SelectionExtendDirection.backward: - return SelectionResult.previous; - case SelectionExtendDirection.nextLine: - case SelectionExtendDirection.forward: - return SelectionResult.next; - } - } - final double baselineInParagraphCoordinates = MatrixUtils.transformPoint( - transform, - Offset(horizontalBaseline, 0), - ).dx; - assert(!baselineInParagraphCoordinates.isNaN); - final TextPosition newPosition; - final SelectionResult result; - switch (movement) { - case SelectionExtendDirection.previousLine: - case SelectionExtendDirection.nextLine: - assert(_textSelectionEnd != null && _textSelectionStart != null); - final TextPosition targetedEdge = isExtent - ? _textSelectionEnd! - : _textSelectionStart!; - final MapEntry moveResult = - _handleVerticalMovement( - targetedEdge, - horizontalBaselineInParagraphCoordinates: - baselineInParagraphCoordinates, - below: movement == SelectionExtendDirection.nextLine, - ); - newPosition = moveResult.key; - result = moveResult.value; - case SelectionExtendDirection.forward: - case SelectionExtendDirection.backward: - _textSelectionEnd ??= movement == SelectionExtendDirection.forward - ? TextPosition(offset: range.start) - : TextPosition(offset: range.end, affinity: TextAffinity.upstream); - _textSelectionStart ??= _textSelectionEnd; - final TextPosition targetedEdge = isExtent - ? _textSelectionEnd! - : _textSelectionStart!; - final Offset edgeOffsetInParagraphCoordinates = paragraph - ._getOffsetForPosition( - targetedEdge, - ); - final Offset baselineOffsetInParagraphCoordinates = Offset( - baselineInParagraphCoordinates, - // Use half of line height to point to the middle of the line. - edgeOffsetInParagraphCoordinates.dy - - paragraph._textPainter.preferredLineHeight / 2, - ); - newPosition = paragraph.getPositionForOffset( - baselineOffsetInParagraphCoordinates, - ); - result = SelectionResult.end; - } - if (isExtent) { - _textSelectionEnd = newPosition; - } else { - _textSelectionStart = newPosition; - } - return result; - } - - SelectionResult _handleGranularlyExtendSelection( - bool forward, - bool isExtent, - TextGranularity granularity, - ) { - _textSelectionEnd ??= forward - ? TextPosition(offset: range.start) - : TextPosition(offset: range.end, affinity: TextAffinity.upstream); - _textSelectionStart ??= _textSelectionEnd; - final TextPosition targetedEdge = isExtent - ? _textSelectionEnd! - : _textSelectionStart!; - if (forward && (targetedEdge.offset == range.end)) { - return SelectionResult.next; - } - if (!forward && (targetedEdge.offset == range.start)) { - return SelectionResult.previous; - } - final SelectionResult result; - final TextPosition newPosition; - switch (granularity) { - case TextGranularity.character: - final String text = range.textInside(fullText); - newPosition = _moveBeyondTextBoundaryAtDirection( - targetedEdge, - forward, - CharacterBoundary(text), - ); - result = SelectionResult.end; - case TextGranularity.word: - final TextBoundary textBoundary = - paragraph._textPainter.wordBoundaries.moveByWordBoundary; - newPosition = _moveBeyondTextBoundaryAtDirection( - targetedEdge, - forward, - textBoundary, - ); - result = SelectionResult.end; - case TextGranularity.paragraph: - final String text = range.textInside(fullText); - newPosition = _moveBeyondTextBoundaryAtDirection( - targetedEdge, - forward, - ParagraphBoundary(text), - ); - result = SelectionResult.end; - case TextGranularity.line: - newPosition = _moveToTextBoundaryAtDirection( - targetedEdge, - forward, - LineBoundary(this), - ); - result = SelectionResult.end; - case TextGranularity.document: - final String text = range.textInside(fullText); - newPosition = _moveBeyondTextBoundaryAtDirection( - targetedEdge, - forward, - DocumentBoundary(text), - ); - if (forward && newPosition.offset == range.end) { - result = SelectionResult.next; - } else if (!forward && newPosition.offset == range.start) { - result = SelectionResult.previous; - } else { - result = SelectionResult.end; - } - } - - if (isExtent) { - _textSelectionEnd = newPosition; - } else { - _textSelectionStart = newPosition; - } - return result; - } - - // Move **beyond** the local boundary of the given type (unless range.start or - // range.end is reached). Used for most TextGranularity types except for - // TextGranularity.line, to ensure the selection movement doesn't get stuck at - // a local fixed point. - TextPosition _moveBeyondTextBoundaryAtDirection( - TextPosition end, - bool forward, - TextBoundary textBoundary, - ) { - final int newOffset = forward - ? textBoundary.getTrailingTextBoundaryAt(end.offset) ?? range.end - : textBoundary.getLeadingTextBoundaryAt(end.offset - 1) ?? range.start; - return TextPosition(offset: newOffset); - } - - // Move **to** the local boundary of the given type. Typically used for line - // boundaries, such that performing "move to line start" more than once never - // moves the selection to the previous line. - TextPosition _moveToTextBoundaryAtDirection( - TextPosition end, - bool forward, - TextBoundary textBoundary, - ) { - assert(end.offset >= 0); - final int caretOffset; - switch (end.affinity) { - case TextAffinity.upstream: - if (end.offset < 1 && !forward) { - assert(end.offset == 0); - return const TextPosition(offset: 0); - } - final CharacterBoundary characterBoundary = CharacterBoundary(fullText); - caretOffset = - math.max( - 0, - characterBoundary.getLeadingTextBoundaryAt( - range.start + end.offset, - ) ?? - range.start, - ) - - 1; - case TextAffinity.downstream: - caretOffset = end.offset; - } - final int offset = forward - ? textBoundary.getTrailingTextBoundaryAt(caretOffset) ?? range.end - : textBoundary.getLeadingTextBoundaryAt(caretOffset) ?? range.start; - return TextPosition(offset: offset); - } - - MapEntry _handleVerticalMovement( - TextPosition position, { - required double horizontalBaselineInParagraphCoordinates, - required bool below, - }) { - final List lines = paragraph._textPainter - .computeLineMetrics(); - final Offset offset = paragraph.getOffsetForCaret(position, Rect.zero); - int currentLine = lines.length - 1; - for (final ui.LineMetrics lineMetrics in lines) { - if (lineMetrics.baseline > offset.dy) { - currentLine = lineMetrics.lineNumber; - break; - } - } - final TextPosition newPosition; - if (below && currentLine == lines.length - 1) { - newPosition = TextPosition( - offset: range.end, - affinity: TextAffinity.upstream, - ); - } else if (!below && currentLine == 0) { - newPosition = TextPosition(offset: range.start); - } else { - final int newLine = below ? currentLine + 1 : currentLine - 1; - newPosition = _clampTextPosition( - paragraph.getPositionForOffset( - Offset( - horizontalBaselineInParagraphCoordinates, - lines[newLine].baseline, - ), - ), - ); - } - final SelectionResult result; - if (newPosition.offset == range.start) { - result = SelectionResult.previous; - } else if (newPosition.offset == range.end) { - result = SelectionResult.next; - } else { - result = SelectionResult.end; - } - assert(result != SelectionResult.next || below); - assert(result != SelectionResult.previous || !below); - return MapEntry(newPosition, result); - } - - /// Whether the given text position is contained in current selection - /// range. - /// - /// The parameter `start` must be smaller than `end`. - bool _positionIsWithinCurrentSelection(TextPosition position) { - if (_textSelectionStart == null || _textSelectionEnd == null) { - return false; - } - // Normalize current selection. - late TextPosition currentStart; - late TextPosition currentEnd; - if (_compareTextPositions(_textSelectionStart!, _textSelectionEnd!) > 0) { - currentStart = _textSelectionStart!; - currentEnd = _textSelectionEnd!; - } else { - currentStart = _textSelectionEnd!; - currentEnd = _textSelectionStart!; - } - return _compareTextPositions(currentStart, position) >= 0 && - _compareTextPositions(currentEnd, position) <= 0; - } - - /// Compares two text positions. - /// - /// Returns 1 if `position` < `otherPosition`, -1 if `position` > `otherPosition`, - /// or 0 if they are equal. - static int _compareTextPositions( - TextPosition position, - TextPosition otherPosition, - ) { - if (position.offset < otherPosition.offset) { - return 1; - } else if (position.offset > otherPosition.offset) { - return -1; - } else if (position.affinity == otherPosition.affinity) { - return 0; - } else { - return position.affinity == TextAffinity.upstream ? 1 : -1; - } - } - - @override - Matrix4 getTransformTo(RenderObject? ancestor) { - return paragraph.getTransformTo(ancestor); - } - - @override - void pushHandleLayers(LayerLink? startHandle, LayerLink? endHandle) { - if (!paragraph.attached) { - assert( - startHandle == null && endHandle == null, - 'Only clean up can be called.', - ); - return; - } - if (_startHandleLayerLink != startHandle) { - _startHandleLayerLink = startHandle; - paragraph.markNeedsPaint(); - } - if (_endHandleLayerLink != endHandle) { - _endHandleLayerLink = endHandle; - paragraph.markNeedsPaint(); - } - } - - List? _cachedBoundingBoxes; - - @override - List get boundingBoxes { - if (_cachedBoundingBoxes == null) { - final List boxes = paragraph.getBoxesForSelection( - TextSelection(baseOffset: range.start, extentOffset: range.end), - boxHeightStyle: .max, - ); - if (boxes.isNotEmpty) { - _cachedBoundingBoxes = []; - for (final TextBox textBox in boxes) { - _cachedBoundingBoxes!.add(textBox.toRect()); - } - } else { - final Offset offset = paragraph._getOffsetForPosition( - TextPosition(offset: range.start), - ); - final Rect rect = Rect.fromPoints( - offset, - offset.translate(0, -paragraph._textPainter.preferredLineHeight), - ); - _cachedBoundingBoxes = [rect]; - } - } - return _cachedBoundingBoxes!; - } - - Rect? _cachedRect; - - Rect get _rect { - if (_cachedRect == null) { - final List boxes = paragraph.getBoxesForSelection( - TextSelection(baseOffset: range.start, extentOffset: range.end), - boxHeightStyle: .max, - ); - if (boxes.isNotEmpty) { - Rect result = boxes.first.toRect(); - for (int index = 1; index < boxes.length; index += 1) { - result = result.expandToInclude(boxes[index].toRect()); - } - _cachedRect = result; - } else { - final Offset offset = paragraph._getOffsetForPosition( - TextPosition(offset: range.start), - ); - _cachedRect = Rect.fromPoints( - offset, - offset.translate(0, -paragraph._textPainter.preferredLineHeight), - ); - } - } - return _cachedRect!; - } - - void didChangeParagraphLayout() { - _cachedRect = null; - _cachedBoundingBoxes = null; - } - - @override - int get contentLength => range.end - range.start; - - @override - Size get size { - return _rect.size; - } - - void paint(PaintingContext context, Offset offset) { - if (_textSelectionStart == null || _textSelectionEnd == null) { - return; - } - if (paragraph.selectionColor != null) { - final TextSelection selection = TextSelection( - baseOffset: _textSelectionStart!.offset, - extentOffset: _textSelectionEnd!.offset, - ); - final Paint selectionPaint = Paint() - ..style = PaintingStyle.fill - ..color = paragraph.selectionColor!; - for (final TextBox textBox in paragraph.getBoxesForSelection(selection)) { - context.canvas.drawRect(textBox.toRect().shift(offset), selectionPaint); - } - } - if (_startHandleLayerLink != null && value.startSelectionPoint != null) { - context.pushLayer( - LeaderLayer( - link: _startHandleLayerLink!, - offset: offset + value.startSelectionPoint!.localPosition, - ), - (PaintingContext context, Offset offset) {}, - Offset.zero, - ); - } - if (_endHandleLayerLink != null && value.endSelectionPoint != null) { - context.pushLayer( - LeaderLayer( - link: _endHandleLayerLink!, - offset: offset + value.endSelectionPoint!.localPosition, - ), - (PaintingContext context, Offset offset) {}, - Offset.zero, - ); - } - } - - @override - TextSelection getLineAtOffset(TextPosition position) { - final TextRange line = paragraph._getLineAtOffset(position); - final int start = line.start.clamp(range.start, range.end); - final int end = line.end.clamp(range.start, range.end); - return TextSelection(baseOffset: start, extentOffset: end); - } - - @override - TextPosition getTextPositionAbove(TextPosition position) { - return _clampTextPosition(paragraph._getTextPositionAbove(position)); - } - - @override - TextPosition getTextPositionBelow(TextPosition position) { - return _clampTextPosition(paragraph._getTextPositionBelow(position)); - } - - @override - TextRange getWordBoundary(TextPosition position) => - paragraph.getWordBoundary(position); - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add( - DiagnosticsProperty( - 'textInsideRange', - range.textInside(fullText), - ), - ) - ..add(DiagnosticsProperty('range', range)) - ..add(DiagnosticsProperty('fullText', fullText)); - } -} diff --git a/lib/common/widgets/flutter/text/rich_text.dart b/lib/common/widgets/flutter/text/rich_text.dart deleted file mode 100644 index 7c1b6e66f..000000000 --- a/lib/common/widgets/flutter/text/rich_text.dart +++ /dev/null @@ -1,353 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// ignore_for_file: prefer_initializing_formals - -import 'dart:ui' as ui show TextHeightBehavior; - -import 'package:PiliPlus/common/widgets/flutter/text/paragraph.dart'; -import 'package:flutter/material.dart' hide RichText; -import 'package:flutter/rendering.dart' hide RenderParagraph; - -/// A paragraph of rich text. -/// -/// {@youtube 560 315 https://www.youtube.com/watch?v=rykDVh-QFfw} -/// -/// The [RichText] widget displays text that uses multiple different styles. The -/// text to display is described using a tree of [TextSpan] objects, each of -/// which has an associated style that is used for that subtree. The text might -/// break across multiple lines or might all be displayed on the same line -/// depending on the layout constraints. -/// -/// Text displayed in a [RichText] widget must be explicitly styled. When -/// picking which style to use, consider using [DefaultTextStyle.of] the current -/// [BuildContext] to provide defaults. For more details on how to style text in -/// a [RichText] widget, see the documentation for [TextStyle]. -/// -/// Consider using the [Text] widget to integrate with the [DefaultTextStyle] -/// automatically. When all the text uses the same style, the default constructor -/// is less verbose. The [Text.rich] constructor allows you to style multiple -/// spans with the default text style while still allowing specified styles per -/// span. -/// -/// {@tool snippet} -/// -/// This sample demonstrates how to mix and match text with different text -/// styles using the [RichText] Widget. It displays the text "Hello bold world," -/// emphasizing the word "bold" using a bold font weight. -/// -/// ![](https://flutter.github.io/assets-for-api-docs/assets/widgets/rich_text.png) -/// -/// ```dart -/// RichText( -/// text: TextSpan( -/// text: 'Hello ', -/// style: DefaultTextStyle.of(context).style, -/// children: const [ -/// TextSpan(text: 'bold', style: TextStyle(fontWeight: FontWeight.bold)), -/// TextSpan(text: ' world!'), -/// ], -/// ), -/// ) -/// ``` -/// {@end-tool} -/// -/// ## Selections -/// -/// To make this [RichText] Selectable, the [RichText] needs to be in the -/// subtree of a [SelectionArea] or [SelectableRegion] and a -/// [SelectionRegistrar] needs to be assigned to the -/// [RichText.selectionRegistrar]. One can use -/// [SelectionContainer.maybeOf] to get the [SelectionRegistrar] from a -/// context. This enables users to select the text in [RichText]s with mice or -/// touch events. -/// -/// The [selectionColor] also needs to be set if the selection is enabled to -/// draw the selection highlights. -/// -/// {@tool snippet} -/// -/// This sample demonstrates how to assign a [SelectionRegistrar] for RichTexts -/// in the SelectionArea subtree. -/// -/// ![](https://flutter.github.io/assets-for-api-docs/assets/widgets/rich_text.png) -/// -/// ```dart -/// RichText( -/// text: const TextSpan(text: 'Hello'), -/// selectionRegistrar: SelectionContainer.maybeOf(context), -/// selectionColor: const Color(0xAF6694e8), -/// ) -/// ``` -/// {@end-tool} -/// -/// See also: -/// -/// * [TextStyle], which discusses how to style text. -/// * [TextSpan], which is used to describe the text in a paragraph. -/// * [Text], which automatically applies the ambient styles described by a -/// [DefaultTextStyle] to a single string. -/// * [Text.rich], a const text widget that provides similar functionality -/// as [RichText]. [Text.rich] will inherit [TextStyle] from [DefaultTextStyle]. -/// * [SelectableRegion], which provides an overview of the selection system. -class RichText extends MultiChildRenderObjectWidget { - /// Creates a paragraph of rich text. - /// - /// The [maxLines] property may be null (and indeed defaults to null), but if - /// it is not null, it must be greater than zero. - /// - /// The [textDirection], if null, defaults to the ambient [Directionality], - /// which in that case must not be null. - RichText({ - super.key, - required this.text, - this.textAlign = TextAlign.start, - this.textDirection, - this.softWrap = true, - this.overflow = TextOverflow.clip, - @Deprecated( - 'Use textScaler instead. ' - 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' - 'This feature was deprecated after v3.12.0-2.0.pre.', - ) - double textScaleFactor = 1.0, - TextScaler textScaler = TextScaler.noScaling, - this.maxLines, - this.locale, - this.strutStyle, - this.textWidthBasis = TextWidthBasis.parent, - this.textHeightBehavior, - this.selectionRegistrar, - this.selectionColor, - required this.primary, - this.onShowMore, - }) : assert(maxLines == null || maxLines > 0), - assert(selectionRegistrar == null || selectionColor != null), - assert( - textScaleFactor == 1.0 || identical(textScaler, TextScaler.noScaling), - 'Use textScaler instead.', - ), - textScaler = _effectiveTextScalerFrom(textScaler, textScaleFactor), - super( - children: WidgetSpan.extractFromInlineSpan( - text, - _effectiveTextScalerFrom(textScaler, textScaleFactor), - ), - ); - - static TextScaler _effectiveTextScalerFrom( - TextScaler textScaler, - double textScaleFactor, - ) { - return switch ((textScaler, textScaleFactor)) { - (final TextScaler scaler, 1.0) => scaler, - (TextScaler.noScaling, final double textScaleFactor) => TextScaler.linear( - textScaleFactor, - ), - (final TextScaler scaler, _) => scaler, - }; - } - - /// The text to display in this widget. - final InlineSpan text; - - /// How the text should be aligned horizontally. - final TextAlign textAlign; - - /// The directionality of the text. - /// - /// This decides how [textAlign] values like [TextAlign.start] and - /// [TextAlign.end] are interpreted. - /// - /// This is also used to disambiguate how to render bidirectional text. For - /// example, if the [text] is an English phrase followed by a Hebrew phrase, - /// in a [TextDirection.ltr] context the English phrase will be on the left - /// and the Hebrew phrase to its right, while in a [TextDirection.rtl] - /// context, the English phrase will be on the right and the Hebrew phrase on - /// its left. - /// - /// Defaults to the ambient [Directionality], if any. If there is no ambient - /// [Directionality], then this must not be null. - final TextDirection? textDirection; - - /// Whether the text should break at soft line breaks. - /// - /// If false, the glyphs in the text will be positioned as if there was unlimited horizontal space. - final bool softWrap; - - /// How visual overflow should be handled. - final TextOverflow overflow; - - /// Deprecated. Will be removed in a future version of Flutter. Use - /// [textScaler] instead. - /// - /// The number of font pixels for each logical pixel. - /// - /// For example, if the text scale factor is 1.5, text will be 50% larger than - /// the specified font size. - @Deprecated( - 'Use textScaler instead. ' - 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' - 'This feature was deprecated after v3.12.0-2.0.pre.', - ) - double get textScaleFactor => textScaler.textScaleFactor; - - /// {@macro flutter.painting.textPainter.textScaler} - final TextScaler textScaler; - - /// An optional maximum number of lines for the text to span, wrapping if necessary. - /// If the text exceeds the given number of lines, it will be truncated according - /// to [overflow]. - /// - /// If this is 1, text will not wrap. Otherwise, text will be wrapped at the - /// edge of the box. - final int? maxLines; - - /// Used to select a font when the same Unicode character can - /// be rendered differently, depending on the locale. - /// - /// It's rarely necessary to set this property. By default its value - /// is inherited from the enclosing app with `Localizations.localeOf(context)`. - /// - /// See [RenderParagraph.locale] for more information. - final Locale? locale; - - /// {@macro flutter.painting.textPainter.strutStyle} - final StrutStyle? strutStyle; - - /// {@macro flutter.painting.textPainter.textWidthBasis} - final TextWidthBasis textWidthBasis; - - /// {@macro dart.ui.textHeightBehavior} - final ui.TextHeightBehavior? textHeightBehavior; - - /// The [SelectionRegistrar] this rich text is subscribed to. - /// - /// If this is set, [selectionColor] must be non-null. - final SelectionRegistrar? selectionRegistrar; - - /// The color to use when painting the selection. - /// - /// This is ignored if [selectionRegistrar] is null. - /// - /// See the section on selections in the [RichText] top-level API - /// documentation for more details on enabling selection in [RichText] - /// widgets. - final Color? selectionColor; - - final Color primary; - - final VoidCallback? onShowMore; - - @override - RenderParagraph createRenderObject(BuildContext context) { - assert(textDirection != null || debugCheckHasDirectionality(context)); - return RenderParagraph( - text, - textAlign: textAlign, - textDirection: textDirection ?? Directionality.of(context), - softWrap: softWrap, - overflow: overflow, - textScaler: textScaler, - maxLines: maxLines, - strutStyle: strutStyle, - textWidthBasis: textWidthBasis, - textHeightBehavior: textHeightBehavior, - locale: locale ?? Localizations.maybeLocaleOf(context), - registrar: selectionRegistrar, - selectionColor: selectionColor, - primary: primary, - onShowMore: onShowMore, - ); - } - - @override - void updateRenderObject(BuildContext context, RenderParagraph renderObject) { - assert(textDirection != null || debugCheckHasDirectionality(context)); - renderObject - ..text = (text: text, primary: primary) - ..textAlign = textAlign - ..textDirection = textDirection ?? Directionality.of(context) - ..softWrap = softWrap - ..overflow = overflow - ..textScaler = textScaler - ..maxLines = maxLines - ..strutStyle = strutStyle - ..textWidthBasis = textWidthBasis - ..textHeightBehavior = textHeightBehavior - ..locale = locale ?? Localizations.maybeLocaleOf(context) - ..registrar = selectionRegistrar - ..selectionColor = selectionColor - ..onShowMore = onShowMore; - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add( - EnumProperty( - 'textAlign', - textAlign, - defaultValue: TextAlign.start, - ), - ) - ..add( - EnumProperty( - 'textDirection', - textDirection, - defaultValue: null, - ), - ) - ..add( - FlagProperty( - 'softWrap', - value: softWrap, - ifTrue: 'wrapping at box width', - ifFalse: 'no wrapping except at line break characters', - showName: true, - ), - ) - ..add( - EnumProperty( - 'overflow', - overflow, - defaultValue: TextOverflow.clip, - ), - ) - ..add( - DiagnosticsProperty( - 'textScaler', - textScaler, - defaultValue: TextScaler.noScaling, - ), - ) - ..add(IntProperty('maxLines', maxLines, ifNull: 'unlimited')) - ..add( - EnumProperty( - 'textWidthBasis', - textWidthBasis, - defaultValue: TextWidthBasis.parent, - ), - ) - ..add(StringProperty('text', text.toPlainText())) - ..add( - DiagnosticsProperty('locale', locale, defaultValue: null), - ) - ..add( - DiagnosticsProperty( - 'strutStyle', - strutStyle, - defaultValue: null, - ), - ) - ..add( - DiagnosticsProperty( - 'textHeightBehavior', - textHeightBehavior, - defaultValue: null, - ), - ); - } -} diff --git a/lib/common/widgets/flutter/text/text.dart b/lib/common/widgets/flutter/text/text.dart index 846adba89..b5f91cbc0 100644 --- a/lib/common/widgets/flutter/text/text.dart +++ b/lib/common/widgets/flutter/text/text.dart @@ -14,14 +14,10 @@ /// @docImport 'widget_span.dart'; library; -import 'dart:math'; import 'dart:ui' as ui show TextHeightBehavior; -import 'package:PiliPlus/common/widgets/flutter/text/paragraph.dart'; -import 'package:PiliPlus/common/widgets/flutter/text/rich_text.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart' hide Text, RichText; -import 'package:flutter/rendering.dart' hide RenderParagraph; +import 'package:PiliPlus/common/widgets/more_text/rich_text_more.dart'; +import 'package:flutter/material.dart' hide Text; /// A run of text with a single style. /// @@ -395,7 +391,7 @@ class Text extends StatelessWidget { ); final double? wordSpacing = MediaQuery.maybeWordSpacingOverrideOf(context); final TextSpan effectiveTextSpan = - _OverridingTextStyleTextSpanUtils.applyTextSpacingOverrides( + OverridingTextStyleTextSpanUtils.applyTextSpacingOverrides( lineHeightScaleFactor: lineHeightScaleFactor, letterSpacing: letterSpacing, wordSpacing: wordSpacing, @@ -409,7 +405,6 @@ class Text extends StatelessWidget { final StrutStyle? effectiveStrutStyle = strutStyle?.merge( StrutStyle(height: lineHeightScaleFactor), ); - final SelectionRegistrar? registrar = SelectionContainer.maybeOf(context); final TextScaler textScaler = switch ((this.textScaler, textScaleFactor)) { (final TextScaler textScaler, _) => textScaler, // For unmigrated apps, fall back to textScaleFactor. @@ -418,68 +413,31 @@ class Text extends StatelessWidget { ), (null, null) => MediaQuery.textScalerOf(context), }; - late Widget result; - if (registrar != null) { - result = MouseRegion( - cursor: - DefaultSelectionStyle.of(context).mouseCursor ?? - SystemMouseCursors.text, - child: _SelectableTextContainer( - textAlign: textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start, - textDirection: - textDirection, // RichText uses Directionality.of to obtain a default if this is null. - locale: - locale, // RichText uses Localizations.localeOf to obtain a default if this is null - softWrap: softWrap ?? defaultTextStyle.softWrap, - overflow: - overflow ?? - effectiveTextStyle?.overflow ?? - defaultTextStyle.overflow, - textScaler: textScaler, - maxLines: maxLines ?? defaultTextStyle.maxLines, - strutStyle: effectiveStrutStyle, - textWidthBasis: textWidthBasis ?? defaultTextStyle.textWidthBasis, - textHeightBehavior: - textHeightBehavior ?? - defaultTextStyle.textHeightBehavior ?? - DefaultTextHeightBehavior.maybeOf(context), - selectionColor: - selectionColor ?? - DefaultSelectionStyle.of(context).selectionColor ?? - DefaultSelectionStyle.defaultColor, - text: effectiveTextSpan, - primary: primary, - ), - ); - } else { - result = RichText( - textAlign: textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start, - textDirection: - textDirection, // RichText uses Directionality.of to obtain a default if this is null. - locale: - locale, // RichText uses Localizations.localeOf to obtain a default if this is null - softWrap: softWrap ?? defaultTextStyle.softWrap, - overflow: - overflow ?? - effectiveTextStyle?.overflow ?? - defaultTextStyle.overflow, - textScaler: textScaler, - maxLines: maxLines ?? defaultTextStyle.maxLines, - strutStyle: effectiveStrutStyle, - textWidthBasis: textWidthBasis ?? defaultTextStyle.textWidthBasis, - textHeightBehavior: - textHeightBehavior ?? - defaultTextStyle.textHeightBehavior ?? - DefaultTextHeightBehavior.maybeOf(context), - selectionColor: - selectionColor ?? - DefaultSelectionStyle.of(context).selectionColor ?? - DefaultSelectionStyle.defaultColor, - text: effectiveTextSpan, - primary: primary, - onShowMore: onShowMore, - ); - } + Widget result = RichTextMore( + textAlign: textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start, + textDirection: + textDirection, // RichText uses Directionality.of to obtain a default if this is null. + locale: + locale, // RichText uses Localizations.localeOf to obtain a default if this is null + softWrap: softWrap ?? defaultTextStyle.softWrap, + overflow: + overflow ?? effectiveTextStyle?.overflow ?? defaultTextStyle.overflow, + textScaler: textScaler, + maxLines: maxLines ?? defaultTextStyle.maxLines, + strutStyle: effectiveStrutStyle, + textWidthBasis: textWidthBasis ?? defaultTextStyle.textWidthBasis, + textHeightBehavior: + textHeightBehavior ?? + defaultTextStyle.textHeightBehavior ?? + DefaultTextHeightBehavior.maybeOf(context), + selectionColor: + selectionColor ?? + DefaultSelectionStyle.of(context).selectionColor ?? + DefaultSelectionStyle.defaultColor, + text: effectiveTextSpan, + primary: primary, + onShowMore: onShowMore, + ); if (semanticsLabel != null || semanticsIdentifier != null) { result = Semantics( textDirection: textDirection, @@ -493,853 +451,4 @@ class Text extends StatelessWidget { } return result; } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(StringProperty('data', data, showName: false)); - if (textSpan != null) { - properties.add( - textSpan!.toDiagnosticsNode( - name: 'textSpan', - style: DiagnosticsTreeStyle.transition, - ), - ); - } - style?.debugFillProperties(properties); - properties - ..add( - EnumProperty('textAlign', textAlign, defaultValue: null), - ) - ..add( - EnumProperty( - 'textDirection', - textDirection, - defaultValue: null, - ), - ) - ..add( - DiagnosticsProperty('locale', locale, defaultValue: null), - ) - ..add( - FlagProperty( - 'softWrap', - value: softWrap, - ifTrue: 'wrapping at box width', - ifFalse: 'no wrapping except at line break characters', - showName: true, - ), - ) - ..add( - EnumProperty('overflow', overflow, defaultValue: null), - ) - ..add( - DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: null), - ) - ..add(IntProperty('maxLines', maxLines, defaultValue: null)) - ..add( - EnumProperty( - 'textWidthBasis', - textWidthBasis, - defaultValue: null, - ), - ) - ..add( - DiagnosticsProperty( - 'textHeightBehavior', - textHeightBehavior, - defaultValue: null, - ), - ); - if (semanticsLabel != null) { - properties.add(StringProperty('semanticsLabel', semanticsLabel)); - } - if (semanticsIdentifier != null) { - properties.add( - StringProperty('semanticsIdentifier', semanticsIdentifier), - ); - } - } -} - -class _SelectableTextContainer extends StatefulWidget { - const _SelectableTextContainer({ - required this.text, - required this.textAlign, - this.textDirection, - required this.softWrap, - required this.overflow, - required this.textScaler, - this.maxLines, - this.locale, - this.strutStyle, - required this.textWidthBasis, - this.textHeightBehavior, - required this.selectionColor, - required this.primary, - }); - - final TextSpan text; - final TextAlign textAlign; - final TextDirection? textDirection; - final bool softWrap; - final TextOverflow overflow; - final TextScaler textScaler; - final int? maxLines; - final Locale? locale; - final StrutStyle? strutStyle; - final TextWidthBasis textWidthBasis; - final ui.TextHeightBehavior? textHeightBehavior; - final Color selectionColor; - final Color primary; - - @override - State<_SelectableTextContainer> createState() => - _SelectableTextContainerState(); -} - -class _SelectableTextContainerState extends State<_SelectableTextContainer> { - late final _SelectableTextContainerDelegate _selectionDelegate; - final GlobalKey _textKey = GlobalKey(); - - @override - void initState() { - super.initState(); - _selectionDelegate = _SelectableTextContainerDelegate(_textKey); - } - - @override - void dispose() { - _selectionDelegate.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return SelectionContainer( - delegate: _selectionDelegate, - // Use [_RichText] wrapper so the underlying [RenderParagraph] can register - // its [Selectable]s to the [SelectionContainer] created by this widget. - child: _RichText( - textKey: _textKey, - textAlign: widget.textAlign, - textDirection: widget.textDirection, - locale: widget.locale, - softWrap: widget.softWrap, - overflow: widget.overflow, - textScaler: widget.textScaler, - maxLines: widget.maxLines, - strutStyle: widget.strutStyle, - textWidthBasis: widget.textWidthBasis, - textHeightBehavior: widget.textHeightBehavior, - selectionColor: widget.selectionColor, - text: widget.text, - primary: widget.primary, - ), - ); - } -} - -class _RichText extends StatelessWidget { - const _RichText({ - this.textKey, - required this.text, - required this.textAlign, - this.textDirection, - required this.softWrap, - required this.overflow, - required this.textScaler, - this.maxLines, - this.locale, - this.strutStyle, - required this.textWidthBasis, - this.textHeightBehavior, - required this.selectionColor, - required this.primary, - }); - - final GlobalKey? textKey; - final InlineSpan text; - final TextAlign textAlign; - final TextDirection? textDirection; - final bool softWrap; - final TextOverflow overflow; - final TextScaler textScaler; - final int? maxLines; - final Locale? locale; - final StrutStyle? strutStyle; - final TextWidthBasis textWidthBasis; - final ui.TextHeightBehavior? textHeightBehavior; - final Color selectionColor; - final Color primary; - - @override - Widget build(BuildContext context) { - final SelectionRegistrar? registrar = SelectionContainer.maybeOf(context); - return RichText( - key: textKey, - textAlign: textAlign, - textDirection: textDirection, - locale: locale, - softWrap: softWrap, - overflow: overflow, - textScaler: textScaler, - maxLines: maxLines, - strutStyle: strutStyle, - textWidthBasis: textWidthBasis, - textHeightBehavior: textHeightBehavior, - selectionRegistrar: registrar, - selectionColor: selectionColor, - text: text, - primary: primary, - ); - } -} - -// In practice some selectables like widgetspan shift several pixels. So when -// the vertical position diff is within the threshold, compare the horizontal -// position to make the compareScreenOrder function more robust. -const double _kSelectableVerticalComparingThreshold = 3.0; - -class _SelectableTextContainerDelegate - extends StaticSelectionContainerDelegate { - _SelectableTextContainerDelegate(GlobalKey textKey) : _textKey = textKey; - - final GlobalKey _textKey; - RenderParagraph get paragraph => - _textKey.currentContext!.findRenderObject()! as RenderParagraph; - - @override - SelectionResult handleSelectParagraph(SelectParagraphSelectionEvent event) { - final SelectionResult result = _handleSelectParagraph(event); - super.didReceiveSelectionBoundaryEvents(); - return result; - } - - SelectionResult _handleSelectParagraph(SelectParagraphSelectionEvent event) { - if (event.absorb) { - for (var index = 0; index < selectables.length; index += 1) { - dispatchSelectionEventToChild(selectables[index], event); - } - currentSelectionStartIndex = 0; - currentSelectionEndIndex = selectables.length - 1; - return SelectionResult.next; - } - - // First pass, if the position is on a placeholder then dispatch the selection - // event to the [Selectable] at the location and terminate. - for (var index = 0; index < selectables.length; index += 1) { - final bool selectableIsPlaceholder = !paragraph - .selectableBelongsToParagraph(selectables[index]); - if (selectableIsPlaceholder && - selectables[index].boundingBoxes.isNotEmpty) { - for (final Rect rect in selectables[index].boundingBoxes) { - final Rect globalRect = MatrixUtils.transformRect( - selectables[index].getTransformTo(null), - rect, - ); - if (globalRect.contains(event.globalPosition)) { - currentSelectionStartIndex = currentSelectionEndIndex = index; - return dispatchSelectionEventToChild(selectables[index], event); - } - } - } - } - - SelectionResult? lastSelectionResult; - var foundStart = false; - int? lastNextIndex; - for (var index = 0; index < selectables.length; index += 1) { - if (!paragraph.selectableBelongsToParagraph(selectables[index])) { - if (foundStart) { - final SelectionEvent synthesizedEvent = SelectParagraphSelectionEvent( - globalPosition: event.globalPosition, - absorb: true, - ); - final SelectionResult result = dispatchSelectionEventToChild( - selectables[index], - synthesizedEvent, - ); - if (selectables.length - 1 == index) { - currentSelectionEndIndex = index; - _flushInactiveSelections(); - return result; - } - } - continue; - } - final SelectionGeometry existingGeometry = selectables[index].value; - lastSelectionResult = dispatchSelectionEventToChild( - selectables[index], - event, - ); - if (index == selectables.length - 1 && - lastSelectionResult == SelectionResult.next) { - if (foundStart) { - currentSelectionEndIndex = index; - } else { - currentSelectionStartIndex = currentSelectionEndIndex = index; - } - return SelectionResult.next; - } - if (lastSelectionResult == SelectionResult.next) { - if (selectables[index].value == existingGeometry && !foundStart) { - lastNextIndex = index; - } - if (selectables[index].value != existingGeometry && !foundStart) { - assert(selectables[index].boundingBoxes.isNotEmpty); - assert(selectables[index].value.selectionRects.isNotEmpty); - final bool selectionAtStartOfSelectable = selectables[index] - .boundingBoxes[0] - .overlaps( - selectables[index].value.selectionRects[0], - ); - var startIndex = 0; - if (lastNextIndex != null && selectionAtStartOfSelectable) { - startIndex = lastNextIndex + 1; - } else { - startIndex = lastNextIndex == null && selectionAtStartOfSelectable - ? 0 - : index; - } - for (var i = startIndex; i < index; i += 1) { - final SelectionEvent synthesizedEvent = - SelectParagraphSelectionEvent( - globalPosition: event.globalPosition, - absorb: true, - ); - dispatchSelectionEventToChild(selectables[i], synthesizedEvent); - } - currentSelectionStartIndex = startIndex; - foundStart = true; - } - continue; - } - if (index == 0 && lastSelectionResult == SelectionResult.previous) { - return SelectionResult.previous; - } - if (selectables[index].value != existingGeometry) { - if (!foundStart && lastNextIndex == null) { - currentSelectionStartIndex = 0; - for (var i = 0; i < index; i += 1) { - final SelectionEvent synthesizedEvent = - SelectParagraphSelectionEvent( - globalPosition: event.globalPosition, - absorb: true, - ); - dispatchSelectionEventToChild(selectables[i], synthesizedEvent); - } - } - currentSelectionEndIndex = index; - // Geometry has changed as a result of select paragraph, need to clear the - // selection of other selectables to keep selection in sync. - _flushInactiveSelections(); - } - return SelectionResult.end; - } - assert(lastSelectionResult == null); - return SelectionResult.end; - } - - /// Initializes the selection of the selectable children. - /// - /// The goal is to find the selectable child that contains the selection edge. - /// Returns [SelectionResult.end] if the selection edge ends on any of the - /// children. Otherwise, it returns [SelectionResult.previous] if the selection - /// does not reach any of its children. Returns [SelectionResult.next] - /// if the selection reaches the end of its children. - /// - /// Ideally, this method should only be called twice at the beginning of the - /// drag selection, once for start edge update event, once for end edge update - /// event. - SelectionResult _initSelection( - SelectionEdgeUpdateEvent event, { - required bool isEnd, - }) { - assert( - (isEnd && currentSelectionEndIndex == -1) || - (!isEnd && currentSelectionStartIndex == -1), - ); - SelectionResult? finalResult; - // Begin the search for the selection edge at the opposite edge if it exists. - final hasOppositeEdge = isEnd - ? currentSelectionStartIndex != -1 - : currentSelectionEndIndex != -1; - int newIndex = switch ((isEnd, hasOppositeEdge)) { - (true, true) => currentSelectionStartIndex, - (true, false) => 0, - (false, true) => currentSelectionEndIndex, - (false, false) => 0, - }; - bool? forward; - late SelectionResult currentSelectableResult; - // This loop sends the selection event to one of the following to determine - // the direction of the search. - // - The opposite edge index if it exists. - // - Index 0 if the opposite edge index does not exist. - // - // If the result is `SelectionResult.next`, this loop look backward. - // Otherwise, it looks forward. - // - // The terminate condition are: - // 1. the selectable returns end, pending, none. - // 2. the selectable returns previous when looking forward. - // 2. the selectable returns next when looking backward. - while (newIndex < selectables.length && - newIndex >= 0 && - finalResult == null) { - currentSelectableResult = dispatchSelectionEventToChild( - selectables[newIndex], - event, - ); - switch (currentSelectableResult) { - case SelectionResult.end: - case SelectionResult.pending: - case SelectionResult.none: - finalResult = currentSelectableResult; - case SelectionResult.next: - if (forward == false) { - newIndex += 1; - finalResult = SelectionResult.end; - } else if (newIndex == selectables.length - 1) { - finalResult = currentSelectableResult; - } else { - forward = true; - newIndex += 1; - } - case SelectionResult.previous: - if (forward ?? false) { - newIndex -= 1; - finalResult = SelectionResult.end; - } else if (newIndex == 0) { - finalResult = currentSelectableResult; - } else { - forward = false; - newIndex -= 1; - } - } - } - if (isEnd) { - currentSelectionEndIndex = newIndex; - } else { - currentSelectionStartIndex = newIndex; - } - _flushInactiveSelections(); - return finalResult!; - } - - SelectionResult _adjustSelection( - SelectionEdgeUpdateEvent event, { - required bool isEnd, - }) { - assert(() { - if (isEnd) { - assert( - currentSelectionEndIndex < selectables.length && - currentSelectionEndIndex >= 0, - ); - return true; - } - assert( - currentSelectionStartIndex < selectables.length && - currentSelectionStartIndex >= 0, - ); - return true; - }()); - SelectionResult? finalResult; - // Determines if the edge being adjusted is within the current viewport. - // - If so, we begin the search for the new selection edge position at the - // currentSelectionEndIndex/currentSelectionStartIndex. - // - If not, we attempt to locate the new selection edge starting from - // the opposite end. - // - If neither edge is in the current viewport, the search for the new - // selection edge position begins at 0. - // - // This can happen when there is a scrollable child and the edge being adjusted - // has been scrolled out of view. - final isCurrentEdgeWithinViewport = isEnd - ? value.endSelectionPoint != null - : value.startSelectionPoint != null; - final isOppositeEdgeWithinViewport = isEnd - ? value.startSelectionPoint != null - : value.endSelectionPoint != null; - int newIndex = switch (( - isEnd, - isCurrentEdgeWithinViewport, - isOppositeEdgeWithinViewport, - )) { - (true, true, true) => currentSelectionEndIndex, - (true, true, false) => currentSelectionEndIndex, - (true, false, true) => currentSelectionStartIndex, - (true, false, false) => 0, - (false, true, true) => currentSelectionStartIndex, - (false, true, false) => currentSelectionStartIndex, - (false, false, true) => currentSelectionEndIndex, - (false, false, false) => 0, - }; - bool? forward; - late SelectionResult currentSelectableResult; - // This loop sends the selection event to one of the following to determine - // the direction of the search. - // - currentSelectionEndIndex/currentSelectionStartIndex if the current edge - // is in the current viewport. - // - The opposite edge index if the current edge is not in the current viewport. - // - Index 0 if neither edge is in the current viewport. - // - // If the result is `SelectionResult.next`, this loop look backward. - // Otherwise, it looks forward. - // - // The terminate condition are: - // 1. the selectable returns end, pending, none. - // 2. the selectable returns previous when looking forward. - // 2. the selectable returns next when looking backward. - while (newIndex < selectables.length && - newIndex >= 0 && - finalResult == null) { - currentSelectableResult = dispatchSelectionEventToChild( - selectables[newIndex], - event, - ); - switch (currentSelectableResult) { - case SelectionResult.end: - case SelectionResult.pending: - case SelectionResult.none: - finalResult = currentSelectableResult; - case SelectionResult.next: - if (forward == false) { - newIndex += 1; - finalResult = SelectionResult.end; - } else if (newIndex == selectables.length - 1) { - finalResult = currentSelectableResult; - } else { - forward = true; - newIndex += 1; - } - case SelectionResult.previous: - if (forward ?? false) { - newIndex -= 1; - finalResult = SelectionResult.end; - } else if (newIndex == 0) { - finalResult = currentSelectableResult; - } else { - forward = false; - newIndex -= 1; - } - } - } - if (isEnd) { - final bool forwardSelection = - currentSelectionEndIndex >= currentSelectionStartIndex; - if (forward != null && - ((!forwardSelection && - forward && - newIndex >= currentSelectionStartIndex) || - (forwardSelection && - !forward && - newIndex <= currentSelectionStartIndex))) { - currentSelectionStartIndex = currentSelectionEndIndex; - } - currentSelectionEndIndex = newIndex; - } else { - final bool forwardSelection = - currentSelectionEndIndex >= currentSelectionStartIndex; - if (forward != null && - ((!forwardSelection && - !forward && - newIndex <= currentSelectionEndIndex) || - (forwardSelection && - forward && - newIndex >= currentSelectionEndIndex))) { - currentSelectionEndIndex = currentSelectionStartIndex; - } - currentSelectionStartIndex = newIndex; - } - _flushInactiveSelections(); - return finalResult!; - } - - /// The compare function this delegate used for determining the selection - /// order of the [Selectable]s. - /// - /// Sorts the [Selectable]s by their top left [Rect]. - @override - Comparator get compareOrder => _compareScreenOrder; - - static int _compareScreenOrder(Selectable a, Selectable b) { - // Attempt to sort the selectables under a [_SelectableTextContainerDelegate] - // by the top left rect. - final Rect rectA = MatrixUtils.transformRect( - a.getTransformTo(null), - a.boundingBoxes.first, - ); - final Rect rectB = MatrixUtils.transformRect( - b.getTransformTo(null), - b.boundingBoxes.first, - ); - final int result = _compareVertically(rectA, rectB); - if (result != 0) { - return result; - } - return _compareHorizontally(rectA, rectB); - } - - /// Compares two rectangles in the screen order solely by their vertical - /// positions. - /// - /// Returns positive if a is lower, negative if a is higher, 0 if their - /// order can't be determine solely by their vertical position. - static int _compareVertically(Rect a, Rect b) { - // The rectangles overlap so defer to horizontal comparison. - if ((a.top - b.top < _kSelectableVerticalComparingThreshold && - a.bottom - b.bottom > -_kSelectableVerticalComparingThreshold) || - (b.top - a.top < _kSelectableVerticalComparingThreshold && - b.bottom - a.bottom > -_kSelectableVerticalComparingThreshold)) { - return 0; - } - if ((a.top - b.top).abs() > _kSelectableVerticalComparingThreshold) { - return a.top > b.top ? 1 : -1; - } - return a.bottom > b.bottom ? 1 : -1; - } - - /// Compares two rectangles in the screen order by their horizontal positions - /// assuming one of the rectangles enclose the other rect vertically. - /// - /// Returns positive if a is lower, negative if a is higher. - static int _compareHorizontally(Rect a, Rect b) { - // a encloses b. - if (a.left - b.left < precisionErrorTolerance && - a.right - b.right > -precisionErrorTolerance) { - return -1; - } - // b encloses a. - if (b.left - a.left < precisionErrorTolerance && - b.right - a.right > -precisionErrorTolerance) { - return 1; - } - if ((a.left - b.left).abs() > precisionErrorTolerance) { - return a.left > b.left ? 1 : -1; - } - return a.right > b.right ? 1 : -1; - } - - /// This method calculates a local [SelectedContentRange] based on the list - /// of [selections] that are accumulated from the [Selectable] children under this - /// delegate. This calculation takes into account the accumulated content - /// length before the active selection, and returns null when either selection - /// edge has not been set. - SelectedContentRange? _calculateLocalRange(List<_SelectionInfo> selections) { - if (currentSelectionStartIndex == -1 || currentSelectionEndIndex == -1) { - return null; - } - var startOffset = 0; - var endOffset = 0; - var foundStart = false; - bool forwardSelection = - currentSelectionEndIndex >= currentSelectionStartIndex; - if (currentSelectionEndIndex == currentSelectionStartIndex) { - // Determining selection direction is inaccurate if currentSelectionStartIndex == currentSelectionEndIndex. - // Use the range from the selectable within the selection as the source of truth for selection direction. - final SelectedContentRange rangeAtSelectableInSelection = - selectables[currentSelectionStartIndex].getSelection()!; - forwardSelection = - rangeAtSelectableInSelection.endOffset >= - rangeAtSelectableInSelection.startOffset; - } - for (var index = 0; index < selections.length; index++) { - final _SelectionInfo selection = selections[index]; - if (selection.range == null) { - if (foundStart) { - return SelectedContentRange( - startOffset: forwardSelection ? startOffset : endOffset, - endOffset: forwardSelection ? endOffset : startOffset, - ); - } - startOffset += selection.contentLength; - endOffset = startOffset; - continue; - } - final int selectionStartNormalized = min( - selection.range!.startOffset, - selection.range!.endOffset, - ); - final int selectionEndNormalized = max( - selection.range!.startOffset, - selection.range!.endOffset, - ); - if (!foundStart) { - // Because a RenderParagraph may split its content into multiple selectables - // we have to consider at what offset a selectable starts at relative - // to the RenderParagraph, when the selectable is not the start of the content. - final bool shouldConsiderContentStart = - index > 0 && - paragraph.selectableBelongsToParagraph(selectables[index]); - startOffset += - (selectionStartNormalized - - (shouldConsiderContentStart - ? paragraph - .getPositionForOffset( - selectables[index] - .boundingBoxes - .first - .centerLeft, - ) - .offset - : 0)) - .abs(); - endOffset = - startOffset + - (selectionEndNormalized - selectionStartNormalized).abs(); - foundStart = true; - } else { - endOffset += (selectionEndNormalized - selectionStartNormalized).abs(); - } - } - assert( - foundStart, - 'The start of the selection has not been found despite this selection delegate having an existing currentSelectionStartIndex and currentSelectionEndIndex.', - ); - return SelectedContentRange( - startOffset: forwardSelection ? startOffset : endOffset, - endOffset: forwardSelection ? endOffset : startOffset, - ); - } - - /// Returns a [SelectedContentRange] considering the [SelectedContentRange] - /// from each [Selectable] child managed under this delegate. - /// - /// When nothing is selected or either selection edge has not been set, - /// this method will return `null`. - @override - SelectedContentRange? getSelection() { - final selections = <_SelectionInfo>[ - for (final Selectable selectable in selectables) - ( - contentLength: selectable.contentLength, - range: selectable.getSelection(), - ), - ]; - return _calculateLocalRange(selections); - } - - // From [SelectableRegion]. - - // Clears the selection on all selectables not in the range of - // currentSelectionStartIndex..currentSelectionEndIndex. - // - // If one of the edges does not exist, then this method will clear the selection - // in all selectables except the existing edge. - // - // If neither of the edges exist this method immediately returns. - void _flushInactiveSelections() { - if (currentSelectionStartIndex == -1 && currentSelectionEndIndex == -1) { - return; - } - if (currentSelectionStartIndex == -1 || currentSelectionEndIndex == -1) { - final int skipIndex = currentSelectionStartIndex == -1 - ? currentSelectionEndIndex - : currentSelectionStartIndex; - selectables - .where((Selectable target) => target != selectables[skipIndex]) - .forEach( - (Selectable target) => dispatchSelectionEventToChild( - target, - const ClearSelectionEvent(), - ), - ); - return; - } - final int skipStart = min( - currentSelectionStartIndex, - currentSelectionEndIndex, - ); - final int skipEnd = max( - currentSelectionStartIndex, - currentSelectionEndIndex, - ); - for (var index = 0; index < selectables.length; index += 1) { - if (index >= skipStart && index <= skipEnd) { - continue; - } - dispatchSelectionEventToChild( - selectables[index], - const ClearSelectionEvent(), - ); - } - } - - @override - SelectionResult handleSelectionEdgeUpdate(SelectionEdgeUpdateEvent event) { - if (event.granularity != TextGranularity.paragraph) { - return super.handleSelectionEdgeUpdate(event); - } - updateLastSelectionEdgeLocation( - globalSelectionEdgeLocation: event.globalPosition, - forEnd: event.type == SelectionEventType.endEdgeUpdate, - ); - if (event.type == SelectionEventType.endEdgeUpdate) { - return currentSelectionEndIndex == -1 - ? _initSelection(event, isEnd: true) - : _adjustSelection(event, isEnd: true); - } - return currentSelectionStartIndex == -1 - ? _initSelection(event, isEnd: false) - : _adjustSelection(event, isEnd: false); - } -} - -/// The length of the content that can be selected, and the range that is -/// selected. -typedef _SelectionInfo = ({int contentLength, SelectedContentRange? range}); - -/// A utility class for overriding the text styles of a [TextSpan] tree. -// When changes are made to this class, the equivalent API in editable_text.dart -// must also be updated. -// TODO(Renzo-Olivares): Remove after investigating a solution for overriding all -// styles for children in an [InlineSpan] tree, see: https://github.com/flutter/flutter/issues/177952. -class _OverridingTextStyleTextSpanUtils { - static TextSpan applyTextSpacingOverrides({ - double? lineHeightScaleFactor, - double? letterSpacing, - double? wordSpacing, - required TextSpan textSpan, - }) { - if (lineHeightScaleFactor == null && - letterSpacing == null && - wordSpacing == null) { - return textSpan; - } - return _applyTextStyleOverrides( - TextStyle( - height: lineHeightScaleFactor, - letterSpacing: letterSpacing, - wordSpacing: wordSpacing, - ), - textSpan, - ); - } - - static TextSpan _applyTextStyleOverrides( - TextStyle overrideTextStyle, - TextSpan textSpan, - ) { - return TextSpan( - text: textSpan.text, - children: textSpan.children?.map((InlineSpan child) { - if (child is TextSpan && child.runtimeType == TextSpan) { - return _applyTextStyleOverrides(overrideTextStyle, child); - } - return child; - }).toList(), - style: textSpan.style?.merge(overrideTextStyle) ?? overrideTextStyle, - recognizer: textSpan.recognizer, - mouseCursor: textSpan.mouseCursor, - onEnter: textSpan.onEnter, - onExit: textSpan.onExit, - semanticsLabel: textSpan.semanticsLabel, - semanticsIdentifier: textSpan.semanticsIdentifier, - locale: textSpan.locale, - spellOut: textSpan.spellOut, - ); - } } diff --git a/lib/common/widgets/more_text/paragraph_more.dart b/lib/common/widgets/more_text/paragraph_more.dart new file mode 100644 index 000000000..0ddfceb72 --- /dev/null +++ b/lib/common/widgets/more_text/paragraph_more.dart @@ -0,0 +1,149 @@ +import 'package:flutter/gestures.dart' show TapGestureRecognizer; +import 'package:flutter/rendering.dart' + show + Size, + Color, + Offset, + TextSpan, + TextStyle, + InlineSpan, + TextScaler, + TextPainter, + VoidCallback, + HitTestEntry, + RenderParagraph, + PaintingContext, + BoxHitTestResult, + RenderComparison; + +class RenderParagraphMore extends RenderParagraph { + RenderParagraphMore( + super.text, { + super.textAlign, + required super.textDirection, + super.softWrap, + super.overflow, + // ignore: deprecated_member_use + super.textScaleFactor, + super.textScaler, + super.maxLines, + super.locale, + super.strutStyle, + super.textWidthBasis, + super.textHeightBehavior, + super.children, + super.selectionColor, + super.registrar, + required this._primary, + this._onShowMore, + }); + + Color _primary; + + VoidCallback? _onShowMore; + set onShowMore(VoidCallback? onShowMore) { + if (_onShowMore != onShowMore) { + _onShowMore = onShowMore; + _tapGestureRecognizer?.onTap = onShowMore; + } + } + + TapGestureRecognizer? _tapGestureRecognizer; + + TextSpan _moreTextSpan([TextStyle? style]) => TextSpan( + style: (style ?? text.style!).copyWith(color: _primary), + text: '查看更多', + recognizer: _tapGestureRecognizer, + ); + TextPainter? _morePainter; + + bool didOverflowHeight = false; + + @override + set textScaler(TextScaler value) { + if (textPainter.textScaler == value) { + return; + } + _morePainter + ?..textScaler = value + ..layout(); + super.textScaler = value; + } + + void setText(({InlineSpan text, Color primary}) params) { + final newText = params.text; + _primary = params.primary; + if (_morePainter != null) { + final textSpan = _moreTextSpan(newText.style); + switch (_morePainter!.text!.compareTo(textSpan)) { + case RenderComparison.paint: + _morePainter!.text = textSpan; + case RenderComparison.layout: + _morePainter! + ..text = textSpan + ..layout(); + default: + } + } + super.text = newText; + } + + @override + void performLayout() { + super.performLayout(); + + if (didOverflowHeight = textPainter.didExceedMaxLines) { + if (_onShowMore != null) { + _tapGestureRecognizer ??= TapGestureRecognizer()..onTap = _onShowMore; + } + _morePainter ??= TextPainter( + text: _moreTextSpan(), + textDirection: textDirection, + textScaler: textScaler, + locale: locale, + )..layout(maxWidth: constraints.maxWidth); + size = Size( + size.width, + constraints.constrainHeight(size.height + _morePainter!.height), + ); + } + } + + @override + void paint(PaintingContext context, Offset offset) { + super.paint(context, offset); + + if (didOverflowHeight) { + _morePainter?.paint( + context.canvas, + offset + Offset(0, textPainter.height), + ); + } + } + + @override + bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { + if (_tapGestureRecognizer != null) { + if (_morePainter != null) { + if (position.dx < _morePainter!.width) { + final height = textPainter.height; + if (position.dy > height && + position.dy < height + _morePainter!.height) { + result.add(HitTestEntry(_morePainter!.text as TextSpan)); + return true; + } + } + } + } + return super.hitTestChildren(result, position: position); + } + + @override + void dispose() { + _tapGestureRecognizer?.dispose(); + _tapGestureRecognizer = null; + _morePainter?.dispose(); + _morePainter = null; + super.dispose(); + } +} diff --git a/lib/common/widgets/more_text/rich_text_more.dart b/lib/common/widgets/more_text/rich_text_more.dart new file mode 100644 index 000000000..c452e277c --- /dev/null +++ b/lib/common/widgets/more_text/rich_text_more.dart @@ -0,0 +1,72 @@ +import 'package:PiliPlus/common/widgets/more_text/paragraph_more.dart'; +import 'package:flutter/material.dart'; + +class RichTextMore extends RichText { + RichTextMore({ + super.key, + required super.text, + super.textAlign, + super.textDirection, + super.softWrap, + super.overflow, + // ignore: deprecated_member_use + super.textScaleFactor, + super.textScaler, + super.maxLines, + super.locale, + super.strutStyle, + super.textWidthBasis, + super.textHeightBehavior, + super.selectionRegistrar, + super.selectionColor, + required this.primary, + this.onShowMore, + }); + + final Color primary; + + final VoidCallback? onShowMore; + + @override + RenderParagraphMore createRenderObject(BuildContext context) { + return RenderParagraphMore( + text, + textAlign: textAlign, + textDirection: textDirection ?? Directionality.of(context), + softWrap: softWrap, + overflow: overflow, + textScaler: textScaler, + maxLines: maxLines, + strutStyle: strutStyle, + textWidthBasis: textWidthBasis, + textHeightBehavior: textHeightBehavior, + locale: locale ?? Localizations.maybeLocaleOf(context), + registrar: selectionRegistrar, + selectionColor: selectionColor, + primary: primary, + onShowMore: onShowMore, + ); + } + + @override + void updateRenderObject( + BuildContext context, + RenderParagraphMore renderObject, + ) { + renderObject + ..setText((text: text, primary: primary)) + ..textAlign = textAlign + ..textDirection = textDirection ?? Directionality.of(context) + ..softWrap = softWrap + ..overflow = overflow + ..textScaler = textScaler + ..maxLines = maxLines + ..strutStyle = strutStyle + ..textWidthBasis = textWidthBasis + ..textHeightBehavior = textHeightBehavior + ..locale = locale ?? Localizations.maybeLocaleOf(context) + ..registrar = selectionRegistrar + ..selectionColor = selectionColor + ..onShowMore = onShowMore; + } +} diff --git a/lib/scripts/patch.ps1 b/lib/scripts/patch.ps1 index 15c19bf70..0e46ade18 100644 --- a/lib/scripts/patch.ps1 +++ b/lib/scripts/patch.ps1 @@ -73,6 +73,8 @@ $ScrollableGesturePatch = "lib/scripts/scrollable_gesture.patch" $DraggableScrollableSheetPatch = "lib/scripts/draggable_scrollable_sheet.patch" +$TextPatch = "lib/scripts/text.patch" + # TODO: remove # https://github.com/flutter/flutter/issues/124078 # https://github.com/flutter/flutter/pull/183261 @@ -108,7 +110,7 @@ $patches = @($ModalBarrierPatch, $TextSelectionPatch, $MouseCursorPatch, $PopupMenuPatch, $FABPatch, $NullSafetySelectableRegionPatch, $SelectableRegionPatch, $EditableTextPatch, $TextFieldPatch, $ScrollPositionPatch, $ScrollablePatch, $ScrollableGesturePatch, - $DraggableScrollableSheetPatch, $ScaffoldPatch) + $DraggableScrollableSheetPatch, $ScaffoldPatch, $TextPatch) switch ($platform.ToLower()) { "android" { diff --git a/lib/scripts/text.patch b/lib/scripts/text.patch new file mode 100644 index 000000000..a933dd732 --- /dev/null +++ b/lib/scripts/text.patch @@ -0,0 +1,26 @@ +diff --git a/packages/flutter/lib/src/rendering/paragraph.dart b/packages/flutter/lib/src/rendering/paragraph.dart +index 97a41dafb8d..7d807e1ad2c 100644 +--- a/packages/flutter/lib/src/rendering/paragraph.dart ++++ b/packages/flutter/lib/src/rendering/paragraph.dart +@@ -385,6 +385,7 @@ class RenderParagraph extends RenderBox + ); + + final TextPainter _textPainter; ++ TextPainter get textPainter => _textPainter; + + // Currently, computing min/max intrinsic width/height will destroy state + // inside the painter. Instead of calling _layout again to get back the correct +diff --git a/packages/flutter/lib/src/widgets/text.dart b/packages/flutter/lib/src/widgets/text.dart +index a3aa5fde862..51601f730af 100644 +--- a/packages/flutter/lib/src/widgets/text.dart ++++ b/packages/flutter/lib/src/widgets/text.dart +@@ -1503,6 +1503,9 @@ typedef _SelectionInfo = ({int contentLength, SelectedContentRange? range}); + // must also be updated. + // TODO(Renzo-Olivares): Remove after investigating a solution for overriding all + // styles for children in an [InlineSpan] tree, see: https://github.com/flutter/flutter/issues/177952. ++ ++typedef OverridingTextStyleTextSpanUtils = _OverridingTextStyleTextSpanUtils; ++ + class _OverridingTextStyleTextSpanUtils { + static TextSpan applyTextSpacingOverrides({ + double? lineHeightScaleFactor,