44
55import 'dart:collection' ;
66import 'dart:math' as math;
7- import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle, Gradient, PlaceholderAlignment, Shader, TextBox, TextHeightBehavior;
7+ import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle, Gradient, LineMetrics, PlaceholderAlignment, Shader, TextBox, TextHeightBehavior;
88
99import 'package:flutter/foundation.dart' ;
1010import 'package:flutter/gestures.dart' ;
1111import 'package:flutter/scheduler.dart' ;
1212import 'package:flutter/semantics.dart' ;
13+ import 'package:flutter/services.dart' ;
1314
1415import 'box.dart' ;
1516import 'debug.dart' ;
17+ import 'editable.dart' ;
1618import 'layer.dart' ;
1719import 'object.dart' ;
1820import 'selection.dart' ;
@@ -151,11 +153,11 @@ class RenderParagraph extends RenderBox
151153 _cachedCombinedSemanticsInfos = null ;
152154 _extractPlaceholderSpans (value);
153155 markNeedsLayout ();
156+ _removeSelectionRegistrarSubscription ();
157+ _disposeSelectableFragments ();
158+ _updateSelectionRegistrarSubscription ();
154159 break ;
155160 }
156- _removeSelectionRegistrarSubscription ();
157- _disposeSelectableFragments ();
158- _updateSelectionRegistrarSubscription ();
159161 }
160162
161163 /// The ongoing selections in this paragraph.
@@ -226,7 +228,7 @@ class RenderParagraph extends RenderBox
226228 if (end == - 1 ) {
227229 end = plainText.length;
228230 }
229- result.add (_SelectableFragment (paragraph: this , range: TextRange (start: start, end: end)));
231+ result.add (_SelectableFragment (paragraph: this , range: TextRange (start: start, end: end), fullText : plainText ));
230232 start = end;
231233 }
232234 start += 1 ;
@@ -439,6 +441,10 @@ class RenderParagraph extends RenderBox
439441 return getOffsetForCaret (position, Rect .zero) + Offset (0 , getFullHeightForCaret (position) ?? 0.0 );
440442 }
441443
444+ List <ui.LineMetrics > _computeLineMetrics () {
445+ return _textPainter.computeLineMetrics ();
446+ }
447+
442448 @override
443449 double computeMinIntrinsicWidth (double height) {
444450 if (! _canComputeIntrinsics ()) {
@@ -1027,6 +1033,28 @@ class RenderParagraph extends RenderBox
10271033 return _textPainter.getWordBoundary (position);
10281034 }
10291035
1036+ TextRange _getLineAtOffset (TextPosition position) => _textPainter.getLineBoundary (position);
1037+
1038+ TextPosition _getTextPositionAbove (TextPosition position) {
1039+ // -0.5 of preferredLineHeight points to the middle of the line above.
1040+ final double preferredLineHeight = _textPainter.preferredLineHeight;
1041+ final double verticalOffset = - 0.5 * preferredLineHeight;
1042+ return _getTextPositionVertical (position, verticalOffset);
1043+ }
1044+
1045+ TextPosition _getTextPositionBelow (TextPosition position) {
1046+ // 1.5 of preferredLineHeight points to the middle of the line below.
1047+ final double preferredLineHeight = _textPainter.preferredLineHeight;
1048+ final double verticalOffset = 1.5 * preferredLineHeight;
1049+ return _getTextPositionVertical (position, verticalOffset);
1050+ }
1051+
1052+ TextPosition _getTextPositionVertical (TextPosition position, double verticalOffset) {
1053+ final Offset caretOffset = _textPainter.getOffsetForCaret (position, Rect .zero);
1054+ final Offset caretOffsetTranslated = caretOffset.translate (0.0 , verticalOffset);
1055+ return _textPainter.getPositionForOffset (caretOffsetTranslated);
1056+ }
1057+
10301058 /// Returns the size of the text as laid out.
10311059 ///
10321060 /// This can differ from [size] if the text overflowed or if the [constraints]
@@ -1271,16 +1299,18 @@ class RenderParagraph extends RenderBox
12711299/// [PlaceHolderSpan] . The [RenderParagraph] splits itself on [PlaceHolderSpan]
12721300/// to create multiple `_SelectableFragment` s so that they can be selected
12731301/// separately.
1274- class _SelectableFragment with Selectable , ChangeNotifier {
1302+ class _SelectableFragment with Selectable , ChangeNotifier implements TextLayoutMetrics {
12751303 _SelectableFragment ({
12761304 required this .paragraph,
1305+ required this .fullText,
12771306 required this .range,
12781307 }) : assert (range.isValid && ! range.isCollapsed && range.isNormalized) {
12791308 _selectionGeometry = _getSelectionGeometry ();
12801309 }
12811310
12821311 final TextRange range;
12831312 final RenderParagraph paragraph;
1313+ final String fullText;
12841314
12851315 TextPosition ? _textSelectionStart;
12861316 TextPosition ? _textSelectionEnd;
@@ -1356,6 +1386,22 @@ class _SelectableFragment with Selectable, ChangeNotifier {
13561386 final SelectWordSelectionEvent selectWord = event as SelectWordSelectionEvent ;
13571387 result = _handleSelectWord (selectWord.globalPosition);
13581388 break ;
1389+ case SelectionEventType .granularlyExtendSelection:
1390+ final GranularlyExtendSelectionEvent granularlyExtendSelection = event as GranularlyExtendSelectionEvent ;
1391+ result = _handleGranularlyExtendSelection (
1392+ granularlyExtendSelection.forward,
1393+ granularlyExtendSelection.isEnd,
1394+ granularlyExtendSelection.granularity,
1395+ );
1396+ break ;
1397+ case SelectionEventType .directionallyExtendSelection:
1398+ final DirectionallyExtendSelectionEvent directionallyExtendSelection = event as DirectionallyExtendSelectionEvent ;
1399+ result = _handleDirectionallyExtendSelection (
1400+ directionallyExtendSelection.dx,
1401+ directionallyExtendSelection.isEnd,
1402+ directionallyExtendSelection.direction,
1403+ );
1404+ break ;
13591405 }
13601406
13611407 if (existingSelectionStart != _textSelectionStart ||
@@ -1373,7 +1419,7 @@ class _SelectableFragment with Selectable, ChangeNotifier {
13731419 final int start = math.min (_textSelectionStart! .offset, _textSelectionEnd! .offset);
13741420 final int end = math.max (_textSelectionStart! .offset, _textSelectionEnd! .offset);
13751421 return SelectedContent (
1376- plainText: paragraph.text. toPlainText (includeSemanticsLabels : false ) .substring (start, end),
1422+ plainText: fullText .substring (start, end),
13771423 );
13781424 }
13791425
@@ -1466,6 +1512,155 @@ class _SelectableFragment with Selectable, ChangeNotifier {
14661512 return SelectionResult .end;
14671513 }
14681514
1515+ SelectionResult _handleDirectionallyExtendSelection (double horizontalBaseline, bool isExtent, SelectionExtendDirection movement) {
1516+ final Matrix4 transform = paragraph.getTransformTo (null );
1517+ if (transform.invert () == 0.0 ) {
1518+ switch (movement) {
1519+ case SelectionExtendDirection .previousLine:
1520+ case SelectionExtendDirection .backward:
1521+ return SelectionResult .previous;
1522+ case SelectionExtendDirection .nextLine:
1523+ case SelectionExtendDirection .forward:
1524+ return SelectionResult .next;
1525+ }
1526+ }
1527+ final double baselineInParagraphCoordinates = MatrixUtils .transformPoint (transform, Offset (horizontalBaseline, 0 )).dx;
1528+ assert (! baselineInParagraphCoordinates.isNaN);
1529+ final TextPosition newPosition;
1530+ final SelectionResult result;
1531+ switch (movement) {
1532+ case SelectionExtendDirection .previousLine:
1533+ case SelectionExtendDirection .nextLine:
1534+ assert (_textSelectionEnd != null && _textSelectionStart != null );
1535+ final TextPosition targetedEdge = isExtent ? _textSelectionEnd! : _textSelectionStart! ;
1536+ final MapEntry <TextPosition , SelectionResult > moveResult = _handleVerticalMovement (
1537+ targetedEdge,
1538+ horizontalBaselineInParagraphCoordinates: baselineInParagraphCoordinates,
1539+ below: movement == SelectionExtendDirection .nextLine,
1540+ );
1541+ newPosition = moveResult.key;
1542+ result = moveResult.value;
1543+ break ;
1544+ case SelectionExtendDirection .forward:
1545+ case SelectionExtendDirection .backward:
1546+ _textSelectionEnd ?? = movement == SelectionExtendDirection .forward
1547+ ? TextPosition (offset: range.start)
1548+ : TextPosition (offset: range.end, affinity: TextAffinity .upstream);
1549+ _textSelectionStart ?? = _textSelectionEnd;
1550+ final TextPosition targetedEdge = isExtent ? _textSelectionEnd! : _textSelectionStart! ;
1551+ final Offset edgeOffsetInParagraphCoordinates = paragraph._getOffsetForPosition (targetedEdge);
1552+ final Offset baselineOffsetInParagraphCoordinates = Offset (
1553+ baselineInParagraphCoordinates,
1554+ // Use half of line height to point to the middle of the line.
1555+ edgeOffsetInParagraphCoordinates.dy - paragraph._textPainter.preferredLineHeight / 2 ,
1556+ );
1557+ newPosition = paragraph.getPositionForOffset (baselineOffsetInParagraphCoordinates);
1558+ result = SelectionResult .end;
1559+ break ;
1560+ }
1561+ if (isExtent) {
1562+ _textSelectionEnd = newPosition;
1563+ } else {
1564+ _textSelectionStart = newPosition;
1565+ }
1566+ return result;
1567+ }
1568+
1569+ SelectionResult _handleGranularlyExtendSelection (bool forward, bool isExtent, TextGranularity granularity) {
1570+ _textSelectionEnd ?? = forward
1571+ ? TextPosition (offset: range.start)
1572+ : TextPosition (offset: range.end, affinity: TextAffinity .upstream);
1573+ _textSelectionStart ?? = _textSelectionEnd;
1574+ final TextPosition targetedEdge = isExtent ? _textSelectionEnd! : _textSelectionStart! ;
1575+ if (forward && (targetedEdge.offset == range.end)) {
1576+ return SelectionResult .next;
1577+ }
1578+ if (! forward && (targetedEdge.offset == range.start)) {
1579+ return SelectionResult .previous;
1580+ }
1581+ final SelectionResult result;
1582+ final TextPosition newPosition;
1583+ switch (granularity) {
1584+ case TextGranularity .character:
1585+ final String text = range.textInside (fullText);
1586+ newPosition = _getNextPosition (CharacterBoundary (text), targetedEdge, forward);
1587+ result = SelectionResult .end;
1588+ break ;
1589+ case TextGranularity .word:
1590+ final String text = range.textInside (fullText);
1591+ newPosition = _getNextPosition (WhitespaceBoundary (text) + WordBoundary (this ), targetedEdge, forward);
1592+ result = SelectionResult .end;
1593+ break ;
1594+ case TextGranularity .line:
1595+ newPosition = _getNextPosition (LineBreak (this ), targetedEdge, forward);
1596+ result = SelectionResult .end;
1597+ break ;
1598+ case TextGranularity .document:
1599+ final String text = range.textInside (fullText);
1600+ newPosition = _getNextPosition (DocumentBoundary (text), targetedEdge, forward);
1601+ if (forward && newPosition.offset == range.end) {
1602+ result = SelectionResult .next;
1603+ } else if (! forward && newPosition.offset == range.start) {
1604+ result = SelectionResult .previous;
1605+ } else {
1606+ result = SelectionResult .end;
1607+ }
1608+ break ;
1609+ }
1610+
1611+ if (isExtent) {
1612+ _textSelectionEnd = newPosition;
1613+ } else {
1614+ _textSelectionStart = newPosition;
1615+ }
1616+ return result;
1617+ }
1618+
1619+ TextPosition _getNextPosition (TextBoundary boundary, TextPosition position, bool forward) {
1620+ if (forward) {
1621+ return _clampTextPosition (
1622+ (PushTextPosition .forward + boundary).getTrailingTextBoundaryAt (position)
1623+ );
1624+ }
1625+ return _clampTextPosition (
1626+ (PushTextPosition .backward + boundary).getLeadingTextBoundaryAt (position),
1627+ );
1628+ }
1629+
1630+ MapEntry <TextPosition , SelectionResult > _handleVerticalMovement (TextPosition position, {required double horizontalBaselineInParagraphCoordinates, required bool below}) {
1631+ final List <ui.LineMetrics > lines = paragraph._computeLineMetrics ();
1632+ final Offset offset = paragraph.getOffsetForCaret (position, Rect .zero);
1633+ int currentLine = lines.length - 1 ;
1634+ for (final ui.LineMetrics lineMetrics in lines) {
1635+ if (lineMetrics.baseline > offset.dy) {
1636+ currentLine = lineMetrics.lineNumber;
1637+ break ;
1638+ }
1639+ }
1640+ final TextPosition newPosition;
1641+ if (below && currentLine == lines.length - 1 ) {
1642+ newPosition = TextPosition (offset: range.end, affinity: TextAffinity .upstream);
1643+ } else if (! below && currentLine == 0 ) {
1644+ newPosition = TextPosition (offset: range.start);
1645+ } else {
1646+ final int newLine = below ? currentLine + 1 : currentLine - 1 ;
1647+ newPosition = _clampTextPosition (
1648+ paragraph.getPositionForOffset (Offset (horizontalBaselineInParagraphCoordinates, lines[newLine].baseline))
1649+ );
1650+ }
1651+ final SelectionResult result;
1652+ if (newPosition.offset == range.start) {
1653+ result = SelectionResult .previous;
1654+ } else if (newPosition.offset == range.end) {
1655+ result = SelectionResult .next;
1656+ } else {
1657+ result = SelectionResult .end;
1658+ }
1659+ assert (result != SelectionResult .next || below);
1660+ assert (result != SelectionResult .previous || ! below);
1661+ return MapEntry <TextPosition , SelectionResult >(newPosition, result);
1662+ }
1663+
14691664 /// Whether the given text position is contained in current selection
14701665 /// range.
14711666 ///
@@ -1596,4 +1791,25 @@ class _SelectableFragment with Selectable, ChangeNotifier {
15961791 );
15971792 }
15981793 }
1794+
1795+ @override
1796+ TextSelection getLineAtOffset (TextPosition position) {
1797+ final TextRange line = paragraph._getLineAtOffset (position);
1798+ final int start = line.start.clamp (range.start, range.end); // ignore_clamp_double_lint
1799+ final int end = line.end.clamp (range.start, range.end); // ignore_clamp_double_lint
1800+ return TextSelection (baseOffset: start, extentOffset: end);
1801+ }
1802+
1803+ @override
1804+ TextPosition getTextPositionAbove (TextPosition position) {
1805+ return _clampTextPosition (paragraph._getTextPositionAbove (position));
1806+ }
1807+
1808+ @override
1809+ TextPosition getTextPositionBelow (TextPosition position) {
1810+ return _clampTextPosition (paragraph._getTextPositionBelow (position));
1811+ }
1812+
1813+ @override
1814+ TextRange getWordBoundary (TextPosition position) => paragraph.getWordBoundary (position);
15991815}
0 commit comments