Skip to content

Commit

Permalink
Revert "Characters Package (flutter#53381)" (flutter#59677)
Browse files Browse the repository at this point in the history
This reverts commit e0ed12c.
  • Loading branch information
renyou committed Jun 17, 2020
1 parent 37cb7b7 commit a99d146
Show file tree
Hide file tree
Showing 9 changed files with 331 additions and 737 deletions.
5 changes: 2 additions & 3 deletions packages/flutter/lib/src/material/text_field.dart
Expand Up @@ -6,7 +6,6 @@

import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle;

import 'package:characters/characters.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
Expand Down Expand Up @@ -798,7 +797,7 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe

bool get _isEnabled => widget.enabled ?? widget.decoration?.enabled ?? true;

int get _currentLength => _effectiveController.value.text.characters.length;
int get _currentLength => _effectiveController.value.text.runes.length;

InputDecoration _getEffectiveDecoration() {
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
Expand Down Expand Up @@ -852,7 +851,7 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe
semanticCounterText = localizations.remainingTextFieldCharacterCount(remaining);

// Handle length exceeds maxLength
if (_effectiveController.value.text.characters.length > widget.maxLength) {
if (_effectiveController.value.text.runes.length > widget.maxLength) {
return effectiveDecoration.copyWith(
errorText: effectiveDecoration.errorText ?? '',
counterStyle: effectiveDecoration.errorStyle
Expand Down
10 changes: 7 additions & 3 deletions packages/flutter/lib/src/painting/text_painter.dart
Expand Up @@ -600,15 +600,15 @@ class TextPainter {

// Complex glyphs can be represented by two or more UTF16 codepoints. This
// checks if the value represents a UTF16 glyph by itself or is a 'surrogate'.
static bool _isUtf16Surrogate(int value) {
bool _isUtf16Surrogate(int value) {
return value & 0xF800 == 0xD800;
}

// Checks if the glyph is either [Unicode.RLM] or [Unicode.LRM]. These values take
// up zero space and do not have valid bounding boxes around them.
//
// We do not directly use the [Unicode] constants since they are strings.
static bool _isUnicodeDirectionality(int value) {
bool _isUnicodeDirectionality(int value) {
return value == 0x200F || value == 0x200E;
}

Expand Down Expand Up @@ -637,13 +637,15 @@ class TextPainter {

// Get the Rect of the cursor (in logical pixels) based off the near edge
// of the character upstream from the given string offset.
// TODO(garyq): Use actual extended grapheme cluster length instead of
// an increasing cluster length amount to achieve deterministic performance.
Rect _getRectFromUpstream(int offset, Rect caretPrototype) {
final String flattenedText = _text.toPlainText(includePlaceholders: false);
final int prevCodeUnit = _text.codeUnitAt(max(0, offset - 1));
if (prevCodeUnit == null)
return null;

// Check for multi-code-unit glyphs such as emojis or zero width joiner.
// Check for multi-code-unit glyphs such as emojis or zero width joiner
final bool needsSearch = _isUtf16Surrogate(prevCodeUnit) || _text.codeUnitAt(offset) == _zwjUtf16 || _isUnicodeDirectionality(prevCodeUnit);
int graphemeClusterLength = needsSearch ? 2 : 1;
List<TextBox> boxes = <TextBox>[];
Expand Down Expand Up @@ -686,6 +688,8 @@ class TextPainter {

// Get the Rect of the cursor (in logical pixels) based off the near edge
// of the character downstream from the given string offset.
// TODO(garyq): Use actual extended grapheme cluster length instead of
// an increasing cluster length amount to achieve deterministic performance.
Rect _getRectFromDownstream(int offset, Rect caretPrototype) {
final String flattenedText = _text.toPlainText(includePlaceholders: false);
// We cap the offset at the final index of the _text.
Expand Down
127 changes: 46 additions & 81 deletions packages/flutter/lib/src/rendering/editable.dart
Expand Up @@ -7,7 +7,6 @@
import 'dart:math' as math;
import 'dart:ui' as ui show TextBox, lerpDouble, BoxHeightStyle, BoxWidthStyle;

import 'package:characters/characters.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/semantics.dart';
Expand Down Expand Up @@ -141,6 +140,18 @@ bool _isWhitespace(int codeUnit) {
return true;
}

/// Returns true if [codeUnit] is a leading (high) surrogate for a surrogate
/// pair.
bool _isLeadingSurrogate(int codeUnit) {
return codeUnit & 0xFC00 == 0xD800;
}

/// Returns true if [codeUnit] is a trailing (low) surrogate for a surrogate
/// pair.
bool _isTrailingSurrogate(int codeUnit) {
return codeUnit & 0xFC00 == 0xDC00;
}

/// Displays some text in a scrollable container with a potentially blinking
/// cursor and with gesture recognizers.
///
Expand Down Expand Up @@ -240,7 +251,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
assert(ignorePointer != null),
assert(textWidthBasis != null),
assert(paintCursorAboveText != null),
assert(obscuringCharacter != null && obscuringCharacter.characters.length == 1),
assert(obscuringCharacter != null && obscuringCharacter.length == 1),
assert(obscureText != null),
assert(textSelectionDelegate != null),
assert(cursorWidth != null && cursorWidth >= 0.0),
Expand Down Expand Up @@ -355,7 +366,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
if (_obscuringCharacter == value) {
return;
}
assert(value != null && value.characters.length == 1);
assert(value != null && value.length == 1);
_obscuringCharacter = value;
markNeedsLayout();
}
Expand Down Expand Up @@ -507,6 +518,10 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
..._nonModifierKeys,
};

// TODO(goderbauer): doesn't handle extended grapheme clusters with more than one Unicode scalar value (https://github.com/flutter/flutter/issues/13404).
// This is because some of this code depends upon counting the length of the
// string using Unicode scalar values, rather than using the number of
// extended grapheme clusters (a.k.a. "characters" in the end user's mind).
void _handleKeyEvent(RawKeyEvent keyEvent) {
if(kIsWeb) {
// On web platform, we should ignore the key because it's processed already.
Expand Down Expand Up @@ -542,71 +557,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
}
}

/// Returns the index into the string of the next character boundary after the
/// given index.
///
/// The character boundary is determined by the characters package, so
/// surrogate pairs and extended grapheme clusters are considered.
///
/// The index must be between 0 and string.length, inclusive. If given
/// string.length, string.length is returned.
///
/// Setting includeWhitespace to false will only return the index of non-space
/// characters.
@visibleForTesting
static int nextCharacter(int index, String string, [bool includeWhitespace = true]) {
assert(index >= 0 && index <= string.length);
if (index == string.length) {
return string.length;
}

int count = 0;
final Characters remaining = string.characters.skipWhile((String currentString) {
if (count <= index) {
count += currentString.length;
return true;
}
if (includeWhitespace) {
return false;
}
return _isWhitespace(currentString.characters.first.toString().codeUnitAt(0));
});
return string.length - remaining.toString().length;
}

/// Returns the index into the string of the previous character boundary
/// before the given index.
///
/// The character boundary is determined by the characters package, so
/// surrogate pairs and extended grapheme clusters are considered.
///
/// The index must be between 0 and string.length, inclusive. If index is 0,
/// 0 will be returned.
///
/// Setting includeWhitespace to false will only return the index of non-space
/// characters.
@visibleForTesting
static int previousCharacter(int index, String string, [bool includeWhitespace = true]) {
assert(index >= 0 && index <= string.length);
if (index == 0) {
return 0;
}

int count = 0;
int lastNonWhitespace;
for (final String currentString in string.characters) {
if (!includeWhitespace &&
!_isWhitespace(currentString.characters.first.toString().codeUnitAt(0))) {
lastNonWhitespace = count;
}
if (count + currentString.length >= index) {
return includeWhitespace ? count : lastNonWhitespace ?? 0;
}
count += currentString.length;
}
return 0;
}

void _handleMovement(
LogicalKeyboardKey key, {
@required bool wordModifier,
Expand All @@ -625,6 +575,23 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
final bool upArrow = key == LogicalKeyboardKey.arrowUp;
final bool downArrow = key == LogicalKeyboardKey.arrowDown;

// Find the previous non-whitespace character
int previousNonWhitespace(int extent) {
int result = math.max(extent - 1, 0);
while (result > 0 && _isWhitespace(_plainText.codeUnitAt(result))) {
result -= 1;
}
return result;
}

int nextNonWhitespace(int extent) {
int result = math.min(extent + 1, _plainText.length);
while (result < _plainText.length && _isWhitespace(_plainText.codeUnitAt(result))) {
result += 1;
}
return result;
}

if ((rightArrow || leftArrow) && !(rightArrow && leftArrow)) {
// Jump to begin/end of word.
if (wordModifier) {
Expand All @@ -635,15 +602,15 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
// so we go back to the first non-whitespace before asking for the word
// boundary, since _selectWordAtOffset finds the word boundaries without
// including whitespace.
final int startPoint = previousCharacter(newSelection.extentOffset, _plainText, false);
final int startPoint = previousNonWhitespace(newSelection.extentOffset);
final TextSelection textSelection = _selectWordAtOffset(TextPosition(offset: startPoint));
newSelection = newSelection.copyWith(extentOffset: textSelection.baseOffset);
} else {
// When going right, we want to skip over any whitespace after the word,
// so we go forward to the first non-whitespace character before asking
// for the word bounds, since _selectWordAtOffset finds the word
// boundaries without including whitespace.
final int startPoint = nextCharacter(newSelection.extentOffset, _plainText, false);
final int startPoint = nextNonWhitespace(newSelection.extentOffset);
final TextSelection textSelection = _selectWordAtOffset(TextPosition(offset: startPoint));
newSelection = newSelection.copyWith(extentOffset: textSelection.extentOffset);
}
Expand All @@ -655,32 +622,30 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
// so we go back to the first non-whitespace before asking for the line
// bounds, since _selectLineAtOffset finds the line boundaries without
// including whitespace (like the newline).
final int startPoint = previousCharacter(newSelection.extentOffset, _plainText, false);
final int startPoint = previousNonWhitespace(newSelection.extentOffset);
final TextSelection textSelection = _selectLineAtOffset(TextPosition(offset: startPoint));
newSelection = newSelection.copyWith(extentOffset: textSelection.baseOffset);
} else {
// When going right, we want to skip over any whitespace after the line,
// so we go forward to the first non-whitespace character before asking
// for the line bounds, since _selectLineAtOffset finds the line
// boundaries without including whitespace (like the newline).
final int startPoint = nextCharacter(newSelection.extentOffset, _plainText, false);
final int startPoint = nextNonWhitespace(newSelection.extentOffset);
final TextSelection textSelection = _selectLineAtOffset(TextPosition(offset: startPoint));
newSelection = newSelection.copyWith(extentOffset: textSelection.extentOffset);
}
} else {
if (rightArrow && newSelection.extentOffset < _plainText.length) {
final int nextExtent = nextCharacter(newSelection.extentOffset, _plainText);
final int distance = nextExtent - newSelection.extentOffset;
newSelection = newSelection.copyWith(extentOffset: nextExtent);
final int delta = _isLeadingSurrogate(text.codeUnitAt(newSelection.extentOffset)) ? 2 : 1;
newSelection = newSelection.copyWith(extentOffset: newSelection.extentOffset + delta);
if (shift) {
_cursorResetLocation += distance;
_cursorResetLocation += 1;
}
} else if (leftArrow && newSelection.extentOffset > 0) {
final int previousExtent = previousCharacter(newSelection.extentOffset, _plainText);
final int distance = newSelection.extentOffset - previousExtent;
newSelection = newSelection.copyWith(extentOffset: previousExtent);
final int delta = _isTrailingSurrogate(text.codeUnitAt(newSelection.extentOffset - 1)) ? 2 : 1;
newSelection = newSelection.copyWith(extentOffset: newSelection.extentOffset - delta);
if (shift) {
_cursorResetLocation -= distance;
_cursorResetLocation -= 1;
}
}
}
Expand Down Expand Up @@ -798,7 +763,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
void _handleDelete() {
final String textAfter = selection.textAfter(_plainText);
if (textAfter.isNotEmpty) {
final int deleteCount = nextCharacter(0, textAfter);
final int deleteCount = _isLeadingSurrogate(textAfter.codeUnitAt(0)) ? 2 : 1;
textSelectionDelegate.textEditingValue = TextEditingValue(
text: selection.textBefore(_plainText)
+ selection.textAfter(_plainText).substring(deleteCount),
Expand Down
41 changes: 24 additions & 17 deletions packages/flutter/lib/src/services/text_formatter.dart
Expand Up @@ -6,7 +6,6 @@

import 'dart:math' as math;

import 'package:characters/characters.dart';
import 'package:flutter/foundation.dart' show visibleForTesting;
import 'text_editing.dart';
import 'text_input.dart';
Expand Down Expand Up @@ -170,24 +169,24 @@ class LengthLimitingTextInputFormatter extends TextInputFormatter {
/// characters.
final int maxLength;

/// Truncate the given TextEditingValue to maxLength characters.
///
/// See also:
/// * [Dart's characters package](https://pub.dev/packages/characters).
/// * [Dart's documenetation on runes and grapheme clusters](https://dart.dev/guides/language/language-tour#runes-and-grapheme-clusters).
// TODO(justinmc): This should be updated to use characters instead of runes,
// see the comment in formatEditUpdate.
/// Truncate the given TextEditingValue to maxLength runes.
@visibleForTesting
static TextEditingValue truncate(TextEditingValue value, int maxLength) {
final CharacterRange iterator = CharacterRange(value.text);
if (value.text.characters.length > maxLength) {
iterator.expandNext(maxLength);
}
final String truncated = iterator.current;
final TextSelection newSelection = value.selection.copyWith(
baseOffset: math.min(value.selection.start, maxLength),
extentOffset: math.min(value.selection.end, maxLength),
);
final RuneIterator iterator = RuneIterator(value.text);
if (iterator.moveNext())
for (int count = 0; count < maxLength; ++count)
if (!iterator.moveNext())
break;
final String truncated = value.text.substring(0, iterator.rawIndex);
return TextEditingValue(
text: truncated,
selection: value.selection.copyWith(
baseOffset: math.min(value.selection.start, truncated.length),
extentOffset: math.min(value.selection.end, truncated.length),
),
selection: newSelection,
composing: TextRange.empty,
);
}
Expand All @@ -197,10 +196,18 @@ class LengthLimitingTextInputFormatter extends TextInputFormatter {
TextEditingValue oldValue, // unused.
TextEditingValue newValue,
) {
if (maxLength != null && maxLength > 0 && newValue.text.characters.length > maxLength) {
// This does not count grapheme clusters (i.e. characters visible to the user),
// it counts Unicode runes, which leaves out a number of useful possible
// characters (like many emoji), so this will be inaccurate in the
// presence of those characters. The Dart lang bug
// https://github.com/dart-lang/sdk/issues/28404 has been filed to
// address this in Dart.
// TODO(justinmc): convert this to count actual characters using Dart's
// characters package (https://pub.dev/packages/characters).
if (maxLength != null && maxLength > 0 && newValue.text.runes.length > maxLength) {
// If already at the maximum and tried to enter even more, keep the old
// value.
if (oldValue.text.characters.length == maxLength) {
if (oldValue.text.runes.length == maxLength) {
return oldValue;
}
return truncate(newValue, maxLength);
Expand Down

0 comments on commit a99d146

Please sign in to comment.