4
4
5
5
import 'dart:collection' ;
6
6
import '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;
8
8
9
9
import 'package:flutter/foundation.dart' ;
10
10
import 'package:flutter/gestures.dart' ;
11
11
import 'package:flutter/scheduler.dart' ;
12
12
import 'package:flutter/semantics.dart' ;
13
+ import 'package:flutter/services.dart' ;
13
14
14
15
import 'box.dart' ;
15
16
import 'debug.dart' ;
17
+ import 'editable.dart' ;
16
18
import 'layer.dart' ;
17
19
import 'object.dart' ;
18
20
import 'selection.dart' ;
@@ -151,11 +153,11 @@ class RenderParagraph extends RenderBox
151
153
_cachedCombinedSemanticsInfos = null ;
152
154
_extractPlaceholderSpans (value);
153
155
markNeedsLayout ();
156
+ _removeSelectionRegistrarSubscription ();
157
+ _disposeSelectableFragments ();
158
+ _updateSelectionRegistrarSubscription ();
154
159
break ;
155
160
}
156
- _removeSelectionRegistrarSubscription ();
157
- _disposeSelectableFragments ();
158
- _updateSelectionRegistrarSubscription ();
159
161
}
160
162
161
163
/// The ongoing selections in this paragraph.
@@ -226,7 +228,7 @@ class RenderParagraph extends RenderBox
226
228
if (end == - 1 ) {
227
229
end = plainText.length;
228
230
}
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 ));
230
232
start = end;
231
233
}
232
234
start += 1 ;
@@ -439,6 +441,10 @@ class RenderParagraph extends RenderBox
439
441
return getOffsetForCaret (position, Rect .zero) + Offset (0 , getFullHeightForCaret (position) ?? 0.0 );
440
442
}
441
443
444
+ List <ui.LineMetrics > _computeLineMetrics () {
445
+ return _textPainter.computeLineMetrics ();
446
+ }
447
+
442
448
@override
443
449
double computeMinIntrinsicWidth (double height) {
444
450
if (! _canComputeIntrinsics ()) {
@@ -1027,6 +1033,28 @@ class RenderParagraph extends RenderBox
1027
1033
return _textPainter.getWordBoundary (position);
1028
1034
}
1029
1035
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
+
1030
1058
/// Returns the size of the text as laid out.
1031
1059
///
1032
1060
/// This can differ from [size] if the text overflowed or if the [constraints]
@@ -1271,16 +1299,18 @@ class RenderParagraph extends RenderBox
1271
1299
/// [PlaceHolderSpan] . The [RenderParagraph] splits itself on [PlaceHolderSpan]
1272
1300
/// to create multiple `_SelectableFragment` s so that they can be selected
1273
1301
/// separately.
1274
- class _SelectableFragment with Selectable , ChangeNotifier {
1302
+ class _SelectableFragment with Selectable , ChangeNotifier implements TextLayoutMetrics {
1275
1303
_SelectableFragment ({
1276
1304
required this .paragraph,
1305
+ required this .fullText,
1277
1306
required this .range,
1278
1307
}) : assert (range.isValid && ! range.isCollapsed && range.isNormalized) {
1279
1308
_selectionGeometry = _getSelectionGeometry ();
1280
1309
}
1281
1310
1282
1311
final TextRange range;
1283
1312
final RenderParagraph paragraph;
1313
+ final String fullText;
1284
1314
1285
1315
TextPosition ? _textSelectionStart;
1286
1316
TextPosition ? _textSelectionEnd;
@@ -1356,6 +1386,22 @@ class _SelectableFragment with Selectable, ChangeNotifier {
1356
1386
final SelectWordSelectionEvent selectWord = event as SelectWordSelectionEvent ;
1357
1387
result = _handleSelectWord (selectWord.globalPosition);
1358
1388
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 ;
1359
1405
}
1360
1406
1361
1407
if (existingSelectionStart != _textSelectionStart ||
@@ -1373,7 +1419,7 @@ class _SelectableFragment with Selectable, ChangeNotifier {
1373
1419
final int start = math.min (_textSelectionStart! .offset, _textSelectionEnd! .offset);
1374
1420
final int end = math.max (_textSelectionStart! .offset, _textSelectionEnd! .offset);
1375
1421
return SelectedContent (
1376
- plainText: paragraph.text. toPlainText (includeSemanticsLabels : false ) .substring (start, end),
1422
+ plainText: fullText .substring (start, end),
1377
1423
);
1378
1424
}
1379
1425
@@ -1466,6 +1512,155 @@ class _SelectableFragment with Selectable, ChangeNotifier {
1466
1512
return SelectionResult .end;
1467
1513
}
1468
1514
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
+
1469
1664
/// Whether the given text position is contained in current selection
1470
1665
/// range.
1471
1666
///
@@ -1596,4 +1791,25 @@ class _SelectableFragment with Selectable, ChangeNotifier {
1596
1791
);
1597
1792
}
1598
1793
}
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);
1599
1815
}
0 commit comments