7
7
import 'dart:math' as math;
8
8
import 'dart:ui' as ui show TextBox, lerpDouble, BoxHeightStyle, BoxWidthStyle;
9
9
10
+ import 'package:characters/characters.dart' ;
10
11
import 'package:flutter/foundation.dart' ;
11
12
import 'package:flutter/gestures.dart' ;
12
13
import 'package:flutter/semantics.dart' ;
@@ -140,18 +141,6 @@ bool _isWhitespace(int codeUnit) {
140
141
return true ;
141
142
}
142
143
143
- /// Returns true if [codeUnit] is a leading (high) surrogate for a surrogate
144
- /// pair.
145
- bool _isLeadingSurrogate (int codeUnit) {
146
- return codeUnit & 0xFC00 == 0xD800 ;
147
- }
148
-
149
- /// Returns true if [codeUnit] is a trailing (low) surrogate for a surrogate
150
- /// pair.
151
- bool _isTrailingSurrogate (int codeUnit) {
152
- return codeUnit & 0xFC00 == 0xDC00 ;
153
- }
154
-
155
144
/// Displays some text in a scrollable container with a potentially blinking
156
145
/// cursor and with gesture recognizers.
157
146
///
@@ -251,7 +240,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
251
240
assert (ignorePointer != null ),
252
241
assert (textWidthBasis != null ),
253
242
assert (paintCursorAboveText != null ),
254
- assert (obscuringCharacter != null && obscuringCharacter.length == 1 ),
243
+ assert (obscuringCharacter != null && obscuringCharacter.characters. length == 1 ),
255
244
assert (obscureText != null ),
256
245
assert (textSelectionDelegate != null ),
257
246
assert (cursorWidth != null && cursorWidth >= 0.0 ),
@@ -366,7 +355,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
366
355
if (_obscuringCharacter == value) {
367
356
return ;
368
357
}
369
- assert (value != null && value.length == 1 );
358
+ assert (value != null && value.characters. length == 1 );
370
359
_obscuringCharacter = value;
371
360
markNeedsLayout ();
372
361
}
@@ -518,10 +507,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
518
507
..._nonModifierKeys,
519
508
};
520
509
521
- // TODO(goderbauer): doesn't handle extended grapheme clusters with more than one Unicode scalar value (https://github.com/flutter/flutter/issues/13404).
522
- // This is because some of this code depends upon counting the length of the
523
- // string using Unicode scalar values, rather than using the number of
524
- // extended grapheme clusters (a.k.a. "characters" in the end user's mind).
525
510
void _handleKeyEvent (RawKeyEvent keyEvent) {
526
511
if (kIsWeb) {
527
512
// On web platform, we should ignore the key because it's processed already.
@@ -557,6 +542,71 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
557
542
}
558
543
}
559
544
545
+ /// Returns the index into the string of the next character boundary after the
546
+ /// given index.
547
+ ///
548
+ /// The character boundary is determined by the characters package, so
549
+ /// surrogate pairs and extended grapheme clusters are considered.
550
+ ///
551
+ /// The index must be between 0 and string.length, inclusive. If given
552
+ /// string.length, string.length is returned.
553
+ ///
554
+ /// Setting includeWhitespace to false will only return the index of non-space
555
+ /// characters.
556
+ @visibleForTesting
557
+ static int nextCharacter (int index, String string, [bool includeWhitespace = true ]) {
558
+ assert (index >= 0 && index <= string.length);
559
+ if (index == string.length) {
560
+ return string.length;
561
+ }
562
+
563
+ int count = 0 ;
564
+ final Characters remaining = string.characters.skipWhile ((String currentString) {
565
+ if (count <= index) {
566
+ count += currentString.length;
567
+ return true ;
568
+ }
569
+ if (includeWhitespace) {
570
+ return false ;
571
+ }
572
+ return _isWhitespace (currentString.characters.first.toString ().codeUnitAt (0 ));
573
+ });
574
+ return string.length - remaining.toString ().length;
575
+ }
576
+
577
+ /// Returns the index into the string of the previous character boundary
578
+ /// before the given index.
579
+ ///
580
+ /// The character boundary is determined by the characters package, so
581
+ /// surrogate pairs and extended grapheme clusters are considered.
582
+ ///
583
+ /// The index must be between 0 and string.length, inclusive. If index is 0,
584
+ /// 0 will be returned.
585
+ ///
586
+ /// Setting includeWhitespace to false will only return the index of non-space
587
+ /// characters.
588
+ @visibleForTesting
589
+ static int previousCharacter (int index, String string, [bool includeWhitespace = true ]) {
590
+ assert (index >= 0 && index <= string.length);
591
+ if (index == 0 ) {
592
+ return 0 ;
593
+ }
594
+
595
+ int count = 0 ;
596
+ int lastNonWhitespace;
597
+ for (final String currentString in string.characters) {
598
+ if (! includeWhitespace &&
599
+ ! _isWhitespace (currentString.characters.first.toString ().codeUnitAt (0 ))) {
600
+ lastNonWhitespace = count;
601
+ }
602
+ if (count + currentString.length >= index) {
603
+ return includeWhitespace ? count : lastNonWhitespace ?? 0 ;
604
+ }
605
+ count += currentString.length;
606
+ }
607
+ return 0 ;
608
+ }
609
+
560
610
void _handleMovement (
561
611
LogicalKeyboardKey key, {
562
612
@required bool wordModifier,
@@ -575,23 +625,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
575
625
final bool upArrow = key == LogicalKeyboardKey .arrowUp;
576
626
final bool downArrow = key == LogicalKeyboardKey .arrowDown;
577
627
578
- // Find the previous non-whitespace character
579
- int previousNonWhitespace (int extent) {
580
- int result = math.max (extent - 1 , 0 );
581
- while (result > 0 && _isWhitespace (_plainText.codeUnitAt (result))) {
582
- result -= 1 ;
583
- }
584
- return result;
585
- }
586
-
587
- int nextNonWhitespace (int extent) {
588
- int result = math.min (extent + 1 , _plainText.length);
589
- while (result < _plainText.length && _isWhitespace (_plainText.codeUnitAt (result))) {
590
- result += 1 ;
591
- }
592
- return result;
593
- }
594
-
595
628
if ((rightArrow || leftArrow) && ! (rightArrow && leftArrow)) {
596
629
// Jump to begin/end of word.
597
630
if (wordModifier) {
@@ -602,15 +635,15 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
602
635
// so we go back to the first non-whitespace before asking for the word
603
636
// boundary, since _selectWordAtOffset finds the word boundaries without
604
637
// including whitespace.
605
- final int startPoint = previousNonWhitespace (newSelection.extentOffset);
638
+ final int startPoint = previousCharacter (newSelection.extentOffset, _plainText, false );
606
639
final TextSelection textSelection = _selectWordAtOffset (TextPosition (offset: startPoint));
607
640
newSelection = newSelection.copyWith (extentOffset: textSelection.baseOffset);
608
641
} else {
609
642
// When going right, we want to skip over any whitespace after the word,
610
643
// so we go forward to the first non-whitespace character before asking
611
644
// for the word bounds, since _selectWordAtOffset finds the word
612
645
// boundaries without including whitespace.
613
- final int startPoint = nextNonWhitespace (newSelection.extentOffset);
646
+ final int startPoint = nextCharacter (newSelection.extentOffset, _plainText, false );
614
647
final TextSelection textSelection = _selectWordAtOffset (TextPosition (offset: startPoint));
615
648
newSelection = newSelection.copyWith (extentOffset: textSelection.extentOffset);
616
649
}
@@ -622,30 +655,32 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
622
655
// so we go back to the first non-whitespace before asking for the line
623
656
// bounds, since _selectLineAtOffset finds the line boundaries without
624
657
// including whitespace (like the newline).
625
- final int startPoint = previousNonWhitespace (newSelection.extentOffset);
658
+ final int startPoint = previousCharacter (newSelection.extentOffset, _plainText, false );
626
659
final TextSelection textSelection = _selectLineAtOffset (TextPosition (offset: startPoint));
627
660
newSelection = newSelection.copyWith (extentOffset: textSelection.baseOffset);
628
661
} else {
629
662
// When going right, we want to skip over any whitespace after the line,
630
663
// so we go forward to the first non-whitespace character before asking
631
664
// for the line bounds, since _selectLineAtOffset finds the line
632
665
// boundaries without including whitespace (like the newline).
633
- final int startPoint = nextNonWhitespace (newSelection.extentOffset);
666
+ final int startPoint = nextCharacter (newSelection.extentOffset, _plainText, false );
634
667
final TextSelection textSelection = _selectLineAtOffset (TextPosition (offset: startPoint));
635
668
newSelection = newSelection.copyWith (extentOffset: textSelection.extentOffset);
636
669
}
637
670
} else {
638
671
if (rightArrow && newSelection.extentOffset < _plainText.length) {
639
- final int delta = _isLeadingSurrogate (text.codeUnitAt (newSelection.extentOffset)) ? 2 : 1 ;
640
- newSelection = newSelection.copyWith (extentOffset: newSelection.extentOffset + delta);
672
+ final int nextExtent = nextCharacter (newSelection.extentOffset, _plainText);
673
+ final int distance = nextExtent - newSelection.extentOffset;
674
+ newSelection = newSelection.copyWith (extentOffset: nextExtent);
641
675
if (shift) {
642
- _cursorResetLocation += 1 ;
676
+ _cursorResetLocation += distance ;
643
677
}
644
678
} else if (leftArrow && newSelection.extentOffset > 0 ) {
645
- final int delta = _isTrailingSurrogate (text.codeUnitAt (newSelection.extentOffset - 1 )) ? 2 : 1 ;
646
- newSelection = newSelection.copyWith (extentOffset: newSelection.extentOffset - delta);
679
+ final int previousExtent = previousCharacter (newSelection.extentOffset, _plainText);
680
+ final int distance = newSelection.extentOffset - previousExtent;
681
+ newSelection = newSelection.copyWith (extentOffset: previousExtent);
647
682
if (shift) {
648
- _cursorResetLocation -= 1 ;
683
+ _cursorResetLocation -= distance ;
649
684
}
650
685
}
651
686
}
@@ -763,7 +798,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
763
798
void _handleDelete () {
764
799
final String textAfter = selection.textAfter (_plainText);
765
800
if (textAfter.isNotEmpty) {
766
- final int deleteCount = _isLeadingSurrogate (textAfter. codeUnitAt ( 0 )) ? 2 : 1 ;
801
+ final int deleteCount = nextCharacter ( 0 , textAfter) ;
767
802
textSelectionDelegate.textEditingValue = TextEditingValue (
768
803
text: selection.textBefore (_plainText)
769
804
+ selection.textAfter (_plainText).substring (deleteCount),
0 commit comments