Skip to content

Commit 6b66d79

Browse files
authoredNov 21, 2019
Track and use fallback TextAffinity for null affinity platform TextSelections. (#44622)
1 parent 384a44d commit 6b66d79

File tree

7 files changed

+139
-8
lines changed

7 files changed

+139
-8
lines changed
 

‎packages/flutter/lib/src/painting/text_painter.dart

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -819,9 +819,14 @@ class TextPainter {
819819
return _paragraph.getWordBoundary(position);
820820
}
821821

822-
/// Returns the text range of the line at the given offset.
822+
/// Returns the [TextRange] of the line at the given [TextPosition].
823823
///
824-
/// The newline, if any, is included in the range.
824+
/// The newline, if any, is returned as part of the range.
825+
///
826+
/// Not valid until after layout.
827+
///
828+
/// This can potentially be expensive, since it needs to compute the full
829+
/// layout before it is available.
825830
TextRange getLineBoundary(TextPosition position) {
826831
assert(!_needsLayout);
827832
return _paragraph.getLineBoundary(position);

‎packages/flutter/lib/src/rendering/editable.dart

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,12 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
402402
// down in a multi-line text field when selecting using the keyboard.
403403
bool _wasSelectingVerticallyWithKeyboard = false;
404404

405+
// This is the affinity we use when a platform-supplied value has null
406+
// affinity.
407+
//
408+
// This affinity should never be null.
409+
TextAffinity _fallbackAffinity = TextAffinity.downstream;
410+
405411
// Call through to onSelectionChanged.
406412
void _handleSelectionChange(
407413
TextSelection nextSelection,
@@ -418,6 +424,17 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
418424
}
419425
}
420426

427+
// Sets the fallback affinity to the affinity of the selection.
428+
void _setFallbackAffinity(
429+
TextAffinity affinity,
430+
) {
431+
assert(affinity != null);
432+
// Engine-computed selections will always compute affinity when necessary.
433+
// Cache this affinity in the case where the platform supplied selection
434+
// does not provide an affinity.
435+
_fallbackAffinity = affinity;
436+
}
437+
421438
static final Set<LogicalKeyboardKey> _movementKeys = <LogicalKeyboardKey>{
422439
LogicalKeyboardKey.arrowRight,
423440
LogicalKeyboardKey.arrowLeft,
@@ -963,7 +980,15 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
963980
set selection(TextSelection value) {
964981
if (_selection == value)
965982
return;
966-
_selection = value;
983+
// Use the _fallbackAffinity when the set selection has a null
984+
// affinity. This happens when the platform does not supply affinity,
985+
// in which case using the fallback affinity computed from dart:ui will
986+
// be superior to simply defaulting to TextAffinity.downstream.
987+
if (value.affinity == null) {
988+
_selection = value.copyWith(affinity: _fallbackAffinity);
989+
} else {
990+
_selection = value;
991+
}
967992
_selectionRects = null;
968993
markNeedsPaint();
969994
markNeedsSemanticsUpdate();
@@ -1566,6 +1591,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
15661591
);
15671592
// Call [onSelectionChanged] only when the selection actually changed.
15681593
_handleSelectionChange(newSelection, cause);
1594+
_setFallbackAffinity(newSelection.affinity);
15691595
}
15701596

15711597
/// Select a word around the location of the last tap down.
@@ -1614,15 +1640,18 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
16141640
return;
16151641
}
16161642
final TextPosition position = _textPainter.getPositionForOffset(globalToLocal(_lastTapDownPosition - _paintOffset));
1643+
_setFallbackAffinity(position.affinity);
16171644
final TextRange word = _textPainter.getWordBoundary(position);
1645+
final TextRange lineBoundary = _textPainter.getLineBoundary(position);
1646+
final bool endOfLine = lineBoundary?.end == position.offset && position.affinity != null;
16181647
if (position.offset - word.start <= 1) {
16191648
_handleSelectionChange(
1620-
TextSelection.collapsed(offset: word.start, affinity: TextAffinity.downstream),
1649+
TextSelection.collapsed(offset: word.start, affinity: endOfLine ? position.affinity : TextAffinity.downstream),
16211650
cause,
16221651
);
16231652
} else {
16241653
_handleSelectionChange(
1625-
TextSelection.collapsed(offset: word.end, affinity: TextAffinity.upstream),
1654+
TextSelection.collapsed(offset: word.end, affinity: endOfLine ? position.affinity : TextAffinity.upstream),
16261655
cause,
16271656
);
16281657
}

‎packages/flutter/lib/src/services/text_editing.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ class TextSelection extends TextRange {
8181
/// The position at which the selection originates.
8282
///
8383
/// Might be larger than, smaller than, or equal to extent.
84-
TextPosition get base => TextPosition(offset: baseOffset, affinity: affinity);
84+
TextPosition get base => TextPosition(offset: baseOffset, affinity: affinity ?? TextAffinity.downstream);
8585

8686
/// The position at which the selection terminates.
8787
///
@@ -90,7 +90,7 @@ class TextSelection extends TextRange {
9090
/// side of the selection, this is the location at which to paint the caret.
9191
///
9292
/// Might be larger than, smaller than, or equal to base.
93-
TextPosition get extent => TextPosition(offset: extentOffset, affinity: affinity);
93+
TextPosition get extent => TextPosition(offset: extentOffset, affinity: affinity ?? TextAffinity.downstream);
9494

9595
@override
9696
String toString() {

‎packages/flutter/lib/src/services/text_input.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,9 @@ TextAffinity _toTextAffinity(String affinity) {
472472
case 'TextAffinity.upstream':
473473
return TextAffinity.upstream;
474474
}
475+
// Null affinity indicates that the platform did not provide a valid
476+
// affinity. Set it to null here to allow the framework to supply
477+
// a fallback affinity.
475478
return null;
476479
}
477480

@@ -533,7 +536,7 @@ class TextEditingValue {
533536
selection: TextSelection(
534537
baseOffset: encoded['selectionBase'] ?? -1,
535538
extentOffset: encoded['selectionExtent'] ?? -1,
536-
affinity: _toTextAffinity(encoded['selectionAffinity']) ?? TextAffinity.downstream,
539+
affinity: _toTextAffinity(encoded['selectionAffinity']),
537540
isDirectional: encoded['selectionIsDirectional'] ?? false,
538541
),
539542
composing: TextRange(

‎packages/flutter/test/painting/text_painter_test.dart

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,4 +790,42 @@ void main() {
790790
expect(lines[2].lineNumber, 2);
791791
expect(lines[3].lineNumber, 3);
792792
}, skip: !isLinux);
793+
794+
test('getLineBoundary', () {
795+
final TextPainter painter = TextPainter()
796+
..textDirection = TextDirection.ltr;
797+
798+
const String text = 'test1\nhello line two really long for soft break\nfinal line 4';
799+
painter.text = const TextSpan(
800+
text: text,
801+
);
802+
803+
painter.layout(maxWidth: 300);
804+
805+
final List<ui.LineMetrics> lines = painter.computeLineMetrics();
806+
807+
expect(lines.length, 4);
808+
809+
expect(painter.getLineBoundary(const TextPosition(offset: -1)), const TextRange(start: -1, end: -1));
810+
811+
expect(painter.getLineBoundary(const TextPosition(offset: 0)), const TextRange(start: 0, end: 5));
812+
expect(painter.getLineBoundary(const TextPosition(offset: 1)), const TextRange(start: 0, end: 5));
813+
expect(painter.getLineBoundary(const TextPosition(offset: 4)), const TextRange(start: 0, end: 5));
814+
expect(painter.getLineBoundary(const TextPosition(offset: 5)), const TextRange(start: 0, end: 5));
815+
816+
expect(painter.getLineBoundary(const TextPosition(offset: 10)), const TextRange(start: 6, end: 28));
817+
expect(painter.getLineBoundary(const TextPosition(offset: 15)), const TextRange(start: 6, end: 28));
818+
expect(painter.getLineBoundary(const TextPosition(offset: 21)), const TextRange(start: 6, end: 28));
819+
expect(painter.getLineBoundary(const TextPosition(offset: 28)), const TextRange(start: 6, end: 28));
820+
821+
expect(painter.getLineBoundary(const TextPosition(offset: 29)), const TextRange(start: 28, end: 47));
822+
expect(painter.getLineBoundary(const TextPosition(offset: 47)), const TextRange(start: 28, end: 47));
823+
824+
expect(painter.getLineBoundary(const TextPosition(offset: 48)), const TextRange(start: 48, end: 60));
825+
expect(painter.getLineBoundary(const TextPosition(offset: 49)), const TextRange(start: 48, end: 60));
826+
expect(painter.getLineBoundary(const TextPosition(offset: 60)), const TextRange(start: 48, end: 60));
827+
828+
expect(painter.getLineBoundary(const TextPosition(offset: 61)), const TextRange(start: -1, end: -1));
829+
expect(painter.getLineBoundary(const TextPosition(offset: 100)), const TextRange(start: -1, end: -1));
830+
}, skip: !isLinux);
793831
}

‎packages/flutter/test/rendering/editable_test.dart

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -611,4 +611,29 @@ void main() {
611611
editable.layout(BoxConstraints.loose(const Size(1000.0, 1000.0)));
612612
expect(editable.maxScrollExtent, equals(10));
613613
}, skip: isBrowser); // TODO(yjbanov): https://github.com/flutter/flutter/issues/42772
614+
615+
test('selection affinity uses fallback', () {
616+
final TextSelectionDelegate delegate = FakeEditableTextState();
617+
EditableText.debugDeterministicCursor = true;
618+
619+
final RenderEditable editable = RenderEditable(
620+
textDirection: TextDirection.ltr,
621+
cursorColor: const Color.fromARGB(0xFF, 0xFF, 0x00, 0x00),
622+
offset: ViewportOffset.zero(),
623+
textSelectionDelegate: delegate,
624+
startHandleLayerLink: LayerLink(),
625+
endHandleLayerLink: LayerLink(),
626+
);
627+
628+
expect(editable.selection, null);
629+
630+
const TextSelection sel1 = TextSelection(baseOffset: 10, extentOffset: 11);
631+
editable.selection = sel1;
632+
expect(editable.selection, sel1);
633+
634+
const TextSelection sel2 = TextSelection(baseOffset: 10, extentOffset: 11, affinity: null);
635+
const TextSelection sel3 = TextSelection(baseOffset: 10, extentOffset: 11, affinity: TextAffinity.downstream);
636+
editable.selection = sel2;
637+
expect(editable.selection, sel3);
638+
}, skip: isBrowser);
614639
}

‎packages/flutter/test/services/text_input_test.dart

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,37 @@ void main() {
173173
expect(client.latestMethodCall, 'connectionClosed');
174174
});
175175
});
176+
177+
test('TextEditingValue handles JSON affinity', () async {
178+
final Map<String, dynamic> json = <String, dynamic>{};
179+
json['text'] = 'Xiaomuqiao';
180+
181+
TextEditingValue val = TextEditingValue.fromJSON(json);
182+
expect(val.text, 'Xiaomuqiao');
183+
expect(val.selection.baseOffset, -1);
184+
expect(val.selection.extentOffset, -1);
185+
expect(val.selection.affinity, null);
186+
expect(val.selection.isDirectional, false);
187+
expect(val.composing.start, -1);
188+
expect(val.composing.end, -1);
189+
190+
json['text'] = 'Xiaomuqiao';
191+
json['selectionBase'] = 5;
192+
json['selectionExtent'] = 6;
193+
json['selectionAffinity'] = 'TextAffinity.upstream';
194+
json['selectionIsDirectional'] = true;
195+
json['composingBase'] = 7;
196+
json['composingExtent'] = 8;
197+
198+
val = TextEditingValue.fromJSON(json);
199+
expect(val.text, 'Xiaomuqiao');
200+
expect(val.selection.baseOffset, 5);
201+
expect(val.selection.extentOffset, 6);
202+
expect(val.selection.affinity, TextAffinity.upstream);
203+
expect(val.selection.isDirectional, true);
204+
expect(val.composing.start, 7);
205+
expect(val.composing.end, 8);
206+
});
176207
}
177208

178209
class FakeTextInputClient implements TextInputClient {

0 commit comments

Comments
 (0)
Please sign in to comment.