Unverified Commit 0ba2f6a8 authored by Ricardo Canastro's avatar Ricardo Canastro Committed by GitHub

Support block delete with word and line modifiers (#79695)

Support for keyboard backspace/delete shortcuts
parent e5f75dbe
......@@ -670,10 +670,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
// _handleShortcuts depends on being started in the same stack invocation
// as the _handleKeyEvent method
_handleShortcuts(key);
} else if (key == LogicalKeyboardKey.delete) {
_handleDelete(forward: true);
} else if (key == LogicalKeyboardKey.backspace) {
_handleDelete(forward: false);
}
}
......@@ -1015,11 +1011,90 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
return _getTextPositionVertical(offset, verticalOffset);
}
/// Keeping [selection]'s [TextSelection.baseOffset] fixed, move the
/// [TextSelection.extentOffset] down by one line.
// Deletes the current uncollapsed selection.
void _deleteSelection(TextSelection selection, SelectionChangedCause cause) {
assert(selection.isCollapsed == false);
if (_readOnly || !selection.isValid || selection.isCollapsed) {
return;
}
final String text = textSelectionDelegate.textEditingValue.text;
final String textBefore = selection.textBefore(text);
final String textAfter = selection.textAfter(text);
final int cursorPosition = math.min(selection.start, selection.end);
final TextSelection newSelection = TextSelection.collapsed(offset: cursorPosition);
_setTextEditingValue(
TextEditingValue(text: textBefore + textAfter, selection: newSelection),
cause,
);
}
// Deletes the from the current collapsed selection to the start of the field.
//
// The given SelectionChangedCause indicates the cause of this change and
// will be passed to onSelectionChanged.
//
// See also:
// * _deleteToEnd
void _deleteToStart(TextSelection selection, SelectionChangedCause cause) {
assert(selection.isCollapsed);
if (_readOnly || !selection.isValid) {
return;
}
final String text = textSelectionDelegate.textEditingValue.text;
final String textBefore = selection.textBefore(text);
if (textBefore.isEmpty) {
return;
}
final String textAfter = selection.textAfter(text);
const TextSelection newSelection = TextSelection.collapsed(offset: 0);
_setTextEditingValue(
TextEditingValue(text: textAfter, selection: newSelection),
cause,
);
}
// Deletes the from the current collapsed selection to the end of the field.
//
// The given SelectionChangedCause indicates the cause of this change and
// will be passed to onSelectionChanged.
//
// See also:
// * _deleteToStart
void _deleteToEnd(TextSelection selection, SelectionChangedCause cause) {
assert(selection.isCollapsed);
if (_readOnly || !selection.isValid) {
return;
}
final String text = textSelectionDelegate.textEditingValue.text;
final String textAfter = selection.textAfter(text);
if (textAfter.isEmpty) {
return;
}
final String textBefore = selection.textBefore(text);
final TextSelection newSelection = TextSelection.collapsed(offset: textBefore.length);
_setTextEditingValue(
TextEditingValue(text: textBefore, selection: newSelection),
cause,
);
}
/// Deletes backwards from the current selection.
///
/// If [selectionEnabled] is false, keeps the selection collapsed and just
/// moves it down.
/// If the [selection] is collapsed, deletes a single character before the
/// cursor.
///
/// If the [selection] is not collapsed, deletes the selection.
///
/// {@template flutter.rendering.RenderEditable.cause}
/// The given [SelectionChangedCause] indicates the cause of this change and
......@@ -1028,6 +1103,292 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
///
/// See also:
///
/// * [deleteForward], which is same but in the opposite direction.
void delete(SelectionChangedCause cause) {
assert(_selection != null);
if (_readOnly || !_selection!.isValid) {
return;
}
if (!_selection!.isCollapsed) {
return _deleteSelection(_selection!, cause);
}
final String text = textSelectionDelegate.textEditingValue.text;
String textBefore = _selection!.textBefore(text);
if (textBefore.isEmpty) {
return;
}
final int characterBoundary = previousCharacter(textBefore.length, textBefore);
textBefore = textBefore.substring(0, characterBoundary);
final String textAfter = _selection!.textAfter(text);
final TextSelection newSelection = TextSelection.collapsed(offset: characterBoundary);
_setTextEditingValue(
TextEditingValue(text: textBefore + textAfter, selection: newSelection),
cause,
);
}
/// Deletes a word backwards from the current selection.
///
/// If the [selection] is collapsed, deletes a word before the cursor.
///
/// If the [selection] is not collapsed, deletes the selection.
///
/// If [obscureText] is true, it treats the whole text content as
/// a single word.
///
/// {@macro flutter.rendering.RenderEditable.cause}
///
/// {@template flutter.rendering.RenderEditable.whiteSpace}
/// By default, includeWhitespace is set to true, meaning that whitespace can
/// be considered a word in itself. If set to false, the selection will be
/// extended past any whitespace and the first word following the whitespace.
/// {@endtemplate}
///
/// See also:
///
/// * [deleteForwardByWord], which is same but in the opposite direction.
void deleteByWord(SelectionChangedCause cause, [bool includeWhitespace = true]) {
assert(_selection != null);
if (_readOnly || !_selection!.isValid) {
return;
}
if (!_selection!.isCollapsed) {
return _deleteSelection(_selection!, cause);
}
// When the text is obscured, the whole thing is treated as one big line.
if (obscureText) {
return _deleteToStart(_selection!, cause);
}
final String text = textSelectionDelegate.textEditingValue.text;
String textBefore = _selection!.textBefore(text);
if (textBefore.isEmpty) {
return;
}
final int characterBoundary = _getLeftByWord(_textPainter, textBefore.length, includeWhitespace);
textBefore = textBefore.trimRight().substring(0, characterBoundary);
final String textAfter = _selection!.textAfter(text);
final TextSelection newSelection = TextSelection.collapsed(offset: characterBoundary);
_setTextEditingValue(
TextEditingValue(text: textBefore + textAfter, selection: newSelection),
cause,
);
}
/// Deletes a line backwards from the current selection.
///
/// If the [selection] is collapsed, deletes a line before the cursor.
///
/// If the [selection] is not collapsed, deletes the selection.
///
/// If [obscureText] is true, it treats the whole text content as
/// a single word.
///
/// {@macro flutter.rendering.RenderEditable.cause}
///
/// See also:
///
/// * [deleteForwardByLine], which is same but in the opposite direction.
void deleteByLine(SelectionChangedCause cause) {
assert(_selection != null);
if (_readOnly || !_selection!.isValid) {
return;
}
if (!_selection!.isCollapsed) {
return _deleteSelection(_selection!, cause);
}
// When the text is obscured, the whole thing is treated as one big line.
if (obscureText) {
return _deleteToStart(_selection!, cause);
}
final String text = textSelectionDelegate.textEditingValue.text;
String textBefore = _selection!.textBefore(text);
if (textBefore.isEmpty) {
return;
}
// When there is a line break, line delete shouldn't do anything
final bool isPreviousCharacterBreakLine = textBefore.codeUnitAt(textBefore.length - 1) == 0x0A;
if (isPreviousCharacterBreakLine) {
return;
}
final TextSelection line = _getLineAtOffset(TextPosition(offset: textBefore.length - 1));
textBefore = textBefore.substring(0, line.start);
final String textAfter = _selection!.textAfter(text);
final TextSelection newSelection = TextSelection.collapsed(offset: textBefore.length);
_setTextEditingValue(
TextEditingValue(text: textBefore + textAfter, selection: newSelection),
cause,
);
}
/// Deletes in the foward direction from the current selection.
///
/// If the [selection] is collapsed, deletes a single character after the
/// cursor.
///
/// If the [selection] is not collapsed, deletes the selection.
///
/// {@macro flutter.rendering.RenderEditable.cause}
///
/// See also:
///
/// * [delete], which is same but in the opposite direction.
void deleteForward(SelectionChangedCause cause) {
assert(_selection != null);
if (_readOnly || !_selection!.isValid) {
return;
}
if (!_selection!.isCollapsed) {
return _deleteSelection(_selection!, cause);
}
final String text = textSelectionDelegate.textEditingValue.text;
final String textBefore = _selection!.textBefore(text);
String textAfter = _selection!.textAfter(text);
if (textAfter.isEmpty) {
return;
}
final int deleteCount = nextCharacter(0, textAfter);
textAfter = textAfter.substring(deleteCount);
_setTextEditingValue(
TextEditingValue(text: textBefore + textAfter, selection: _selection!),
cause,
);
}
/// Deletes a word in the foward direction from the current selection.
///
/// If the [selection] is collapsed, deletes a word after the cursor.
///
/// If the [selection] is not collapsed, deletes the selection.
///
/// If [obscureText] is true, it treats the whole text content as
/// a single word.
///
/// {@macro flutter.rendering.RenderEditable.cause}
///
/// {@macro flutter.rendering.RenderEditable.whiteSpace}
///
/// See also:
///
/// * [deleteByWord], which is same but in the opposite direction.
void deleteForwardByWord(SelectionChangedCause cause, [bool includeWhitespace = true]) {
assert(_selection != null);
if (_readOnly || !_selection!.isValid) {
return;
}
if (!_selection!.isCollapsed) {
return _deleteSelection(_selection!, cause);
}
// When the text is obscured, the whole thing is treated as one big word.
if (obscureText) {
return _deleteToEnd(_selection!, cause);
}
final String text = textSelectionDelegate.textEditingValue.text;
String textAfter = _selection!.textAfter(text);
if (textAfter.isEmpty) {
return;
}
final String textBefore = _selection!.textBefore(text);
final int characterBoundary = _getRightByWord(_textPainter, textBefore.length, includeWhitespace);
textAfter = textAfter.substring(characterBoundary - textBefore.length);
_setTextEditingValue(
TextEditingValue(text: textBefore + textAfter, selection: _selection!),
cause,
);
}
/// Deletes a line in the foward direction from the current selection.
///
/// If the [selection] is collapsed, deletes a line after the cursor.
///
/// If the [selection] is not collapsed, deletes the selection.
///
/// If [obscureText] is true, it treats the whole text content as
/// a single word.
///
/// {@macro flutter.rendering.RenderEditable.cause}
///
/// See also:
///
/// * [deleteByLine], which is same but in the opposite direction.
void deleteForwardByLine(SelectionChangedCause cause) {
assert(_selection != null);
if (_readOnly || !_selection!.isValid) {
return;
}
if (!_selection!.isCollapsed) {
return _deleteSelection(_selection!, cause);
}
// When the text is obscured, the whole thing is treated as one big line.
if (obscureText) {
return _deleteToEnd(_selection!, cause);
}
final String text = textSelectionDelegate.textEditingValue.text;
String textAfter = _selection!.textAfter(text);
if (textAfter.isEmpty) {
return;
}
// When there is a line break, it shouldn't do anything.
final bool isNextCharacterBreakLine = textAfter.codeUnitAt(0) == 0x0A;
if (isNextCharacterBreakLine) {
return;
}
final String textBefore = _selection!.textBefore(text);
final TextSelection line = _getLineAtOffset(TextPosition(offset: textBefore.length));
textAfter = textAfter.substring(line.end - textBefore.length, textAfter.length);
_setTextEditingValue(
TextEditingValue(text: textBefore + textAfter, selection: _selection!),
cause,
);
}
/// Keeping [selection]'s [TextSelection.baseOffset] fixed, move the
/// [TextSelection.extentOffset] down by one line.
///
/// If [selectionEnabled] is false, keeps the selection collapsed and just
/// moves it down.
///
/// {@macro flutter.rendering.RenderEditable.cause}
///
/// See also:
///
/// * [extendSelectionUp], which is same but in the opposite direction.
void extendSelectionDown(SelectionChangedCause cause) {
assert(selection != null);
......@@ -1375,10 +1736,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
///
/// {@macro flutter.rendering.RenderEditable.cause}
///
/// By default, `includeWhitespace` is set to true, meaning that whitespace
/// can be considered a word in itself. If set to false, the selection will
/// be extended past any whitespace and the first word following the
/// whitespace.
/// {@macro flutter.rendering.RenderEditable.whiteSpace}
///
/// {@template flutter.rendering.RenderEditable.stopAtReversal}
/// The `stopAtReversal` parameter is false by default, meaning that it's
......@@ -1420,13 +1778,11 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
///
/// {@macro flutter.rendering.RenderEditable.cause}
///
/// By default, `includeWhitespace` is set to true, meaning that whitespace
/// can be considered a word in itself. If set to false, the selection will
/// be extended past any whitespace and the first word following the
/// whitespace.
/// {@macro flutter.rendering.RenderEditable.whiteSpace}
///
/// {@macro flutter.rendering.RenderEditable.stopAtReversal}
///
///
/// See also:
///
/// * [extendSelectionLeftByWord], which is the same but in the opposite
......@@ -1585,9 +1941,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
///
/// {@macro flutter.rendering.RenderEditable.cause}
///
/// By default, includeWhitespace is set to true, meaning that whitespace can
/// be considered a word in itself. If set to false, the selection will be
/// moved past any whitespace and the first word following the whitespace.
/// {@macro flutter.rendering.RenderEditable.whiteSpace}
///
/// See also:
///
......@@ -1673,9 +2027,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
///
/// {@macro flutter.rendering.RenderEditable.cause}
///
/// By default, includeWhitespace is set to true, meaning that whitespace can
/// be considered a word in itself. If set to false, the selection will be
/// moved past any whitespace and the first word following the whitespace.
/// {@macro flutter.rendering.RenderEditable.whiteSpace}
///
/// See also:
///
......@@ -1825,38 +2177,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
}
}
void _handleDelete({ required bool forward }) {
final TextSelection selection = textSelectionDelegate.textEditingValue.selection;
final String text = textSelectionDelegate.textEditingValue.text;
assert(_selection != null);
if (_readOnly || !selection.isValid) {
return;
}
String textBefore = selection.textBefore(text);
String textAfter = selection.textAfter(text);
int cursorPosition = math.min(selection.start, selection.end);
// If not deleting a selection, delete the next/previous character.
if (selection.isCollapsed) {
if (!forward && textBefore.isNotEmpty) {
final int characterBoundary = previousCharacter(textBefore.length, textBefore);
textBefore = textBefore.substring(0, characterBoundary);
cursorPosition = characterBoundary;
}
if (forward && textAfter.isNotEmpty) {
final int deleteCount = nextCharacter(0, textAfter);
textAfter = textAfter.substring(deleteCount);
}
}
final TextSelection newSelection = TextSelection.collapsed(offset: cursorPosition);
_setTextEditingValue(
TextEditingValue(
text: textBefore + textAfter,
selection: newSelection,
),
SelectionChangedCause.keyboard,
);
}
@override
void markNeedsPaint() {
super.markNeedsPaint();
......
......@@ -36,6 +36,12 @@ class DefaultTextEditingActions extends Actions{
// are called on which platform.
static final Map<Type, Action<Intent>> _shortcutsActions = <Type, Action<Intent>>{
DoNothingAndStopPropagationTextIntent: _DoNothingAndStopPropagationTextAction(),
DeleteTextIntent: _DeleteTextAction(),
DeleteByWordTextIntent: _DeleteByWordTextAction(),
DeleteByLineTextIntent: _DeleteByLineTextAction(),
DeleteForwardTextIntent: _DeleteForwardTextAction(),
DeleteForwardByWordTextIntent: _DeleteForwardByWordTextAction(),
DeleteForwardByLineTextIntent: _DeleteForwardByLineTextAction(),
ExtendSelectionDownTextIntent: _ExtendSelectionDownTextAction(),
ExtendSelectionLeftByLineTextIntent: _ExtendSelectionLeftByLineTextAction(),
ExtendSelectionLeftByWordTextIntent: _ExtendSelectionLeftByWordTextAction(),
......@@ -76,6 +82,48 @@ class _DoNothingAndStopPropagationTextAction extends TextEditingAction<DoNothing
void invoke(DoNothingAndStopPropagationTextIntent intent, [BuildContext? context]) {}
}
class _DeleteTextAction extends TextEditingAction<DeleteTextIntent> {
@override
Object? invoke(DeleteTextIntent intent, [BuildContext? context]) {
textEditingActionTarget!.renderEditable.delete(SelectionChangedCause.keyboard);
}
}
class _DeleteByWordTextAction extends TextEditingAction<DeleteByWordTextIntent> {
@override
Object? invoke(DeleteByWordTextIntent intent, [BuildContext? context]) {
textEditingActionTarget!.renderEditable.deleteByWord(SelectionChangedCause.keyboard, false);
}
}
class _DeleteByLineTextAction extends TextEditingAction<DeleteByLineTextIntent> {
@override
Object? invoke(DeleteByLineTextIntent intent, [BuildContext? context]) {
textEditingActionTarget!.renderEditable.deleteByLine(SelectionChangedCause.keyboard);
}
}
class _DeleteForwardTextAction extends TextEditingAction<DeleteForwardTextIntent> {
@override
Object? invoke(DeleteForwardTextIntent intent, [BuildContext? context]) {
textEditingActionTarget!.renderEditable.deleteForward(SelectionChangedCause.keyboard);
}
}
class _DeleteForwardByWordTextAction extends TextEditingAction<DeleteForwardByWordTextIntent> {
@override
Object? invoke(DeleteForwardByWordTextIntent intent, [BuildContext? context]) {
textEditingActionTarget!.renderEditable.deleteForwardByWord(SelectionChangedCause.keyboard, false);
}
}
class _DeleteForwardByLineTextAction extends TextEditingAction<DeleteForwardByLineTextIntent> {
@override
Object? invoke(DeleteForwardByLineTextIntent intent, [BuildContext? context]) {
textEditingActionTarget!.renderEditable.deleteForwardByLine(SelectionChangedCause.keyboard);
}
}
class _ExpandSelectionLeftByLineTextAction extends TextEditingAction<ExpandSelectionLeftByLineTextIntent> {
@override
Object? invoke(ExpandSelectionLeftByLineTextIntent intent, [BuildContext? context]) {
......
......@@ -161,6 +161,12 @@ class DefaultTextEditingShortcuts extends Shortcuts {
);
static final Map<LogicalKeySet, Intent> _androidShortcuts = <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.backspace): const DeleteTextIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.backspace): const DeleteByWordTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.backspace): const DeleteByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.delete): const DeleteForwardTextIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.delete): const DeleteForwardByWordTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.delete): const DeleteForwardByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowDown): const MoveSelectionToEndTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowLeft): const MoveSelectionLeftByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowRight): const MoveSelectionRightByLineTextIntent(),
......@@ -195,9 +201,17 @@ class DefaultTextEditingShortcuts extends Shortcuts {
// * Meta + shift + arrow up
// * Shift + end
// * Shift + home
// * Meta + delete
// * Meta + backspace
};
static final Map<LogicalKeySet, Intent> _fuchsiaShortcuts = <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.backspace): const DeleteTextIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.backspace): const DeleteByWordTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.backspace): const DeleteByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.delete): const DeleteForwardTextIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.delete): const DeleteForwardByWordTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.delete): const DeleteForwardByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowDown): const MoveSelectionToEndTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowLeft): const MoveSelectionLeftByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowRight): const MoveSelectionRightByLineTextIntent(),
......@@ -232,9 +246,17 @@ class DefaultTextEditingShortcuts extends Shortcuts {
// * Meta + shift + arrow up
// * Shift + end
// * Shift + home
// * Meta + delete
// * Meta + backspace
};
static final Map<LogicalKeySet, Intent> _iOSShortcuts = <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.backspace): const DeleteTextIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.backspace): const DeleteByWordTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.backspace): const DeleteByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.delete): const DeleteForwardTextIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.delete): const DeleteForwardByWordTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.delete): const DeleteForwardByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowDown): const MoveSelectionToEndTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowLeft): const MoveSelectionLeftByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowRight): const MoveSelectionRightByLineTextIntent(),
......@@ -269,9 +291,17 @@ class DefaultTextEditingShortcuts extends Shortcuts {
// * Meta + shift + arrow up
// * Shift + end
// * Shift + home
// * Meta + delete
// * Meta + backspace
};
static final Map<LogicalKeySet, Intent> _linuxShortcuts = <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.backspace): const DeleteTextIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.backspace): const DeleteByWordTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.backspace): const DeleteByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.delete): const DeleteForwardTextIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.delete): const DeleteForwardByWordTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.delete): const DeleteForwardByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowDown): const MoveSelectionToEndTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowLeft): const MoveSelectionLeftByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowRight): const MoveSelectionRightByLineTextIntent(),
......@@ -306,9 +336,17 @@ class DefaultTextEditingShortcuts extends Shortcuts {
// * Meta + shift + arrow up
// * Shift + end
// * Shift + home
// * Meta + delete
// * Meta + backspace
};
static final Map<LogicalKeySet, Intent> _macShortcuts = <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.backspace): const DeleteTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.backspace): const DeleteByWordTextIntent(),
LogicalKeySet(LogicalKeyboardKey.meta, LogicalKeyboardKey.backspace): const DeleteByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.delete): const DeleteForwardTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.delete): const DeleteForwardByWordTextIntent(),
LogicalKeySet(LogicalKeyboardKey.meta, LogicalKeyboardKey.delete): const DeleteForwardByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowDown): const MoveSelectionRightByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowLeft): const MoveSelectionLeftByWordTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowRight): const MoveSelectionRightByWordTextIntent(),
......@@ -343,9 +381,17 @@ class DefaultTextEditingShortcuts extends Shortcuts {
// * Home
// * Shift + end
// * Shift + home
// * Control + delete
// * Control + backspace
};
static final Map<LogicalKeySet, Intent> _windowsShortcuts = <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.backspace): const DeleteTextIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.backspace): const DeleteByWordTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.backspace): const DeleteByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.delete): const DeleteForwardTextIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.delete): const DeleteForwardByWordTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.delete): const DeleteForwardByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowDown): const MoveSelectionToEndTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowLeft): const MoveSelectionLeftByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowRight): const MoveSelectionRightByLineTextIntent(),
......@@ -380,11 +426,21 @@ class DefaultTextEditingShortcuts extends Shortcuts {
// * Meta + shift + arrow left
// * Meta + shift + arrow right
// * Meta + shift + arrow up
// * Meta + delete
// * Meta + backspace
};
// Web handles its text selection natively and doesn't use any of these
// shortcuts in Flutter.
static final Map<LogicalKeySet, Intent> _webShortcuts = <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.backspace): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.delete): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.backspace): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.delete): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.backspace): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.delete): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.meta, LogicalKeyboardKey.backspace): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.meta, LogicalKeyboardKey.delete): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowDown): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowLeft): const DoNothingAndStopPropagationTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowRight): const DoNothingAndStopPropagationTextIntent(),
......
......@@ -4,6 +4,54 @@
import 'actions.dart';
/// An [Intent] to delete a character in the backwards direction.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class DeleteTextIntent extends Intent{
/// Creates an instance of DeleteTextIntent.
const DeleteTextIntent();
}
/// An [Intent] to delete a word in the backwards direction.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class DeleteByWordTextIntent extends Intent{
/// Creates an instance of DeleteByWordTextIntent.
const DeleteByWordTextIntent();
}
/// An [Intent] to delete a line in the backwards direction.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class DeleteByLineTextIntent extends Intent{
/// Creates an instance of DeleteByLineTextIntent.
const DeleteByLineTextIntent();
}
/// An [Intent] to delete in the forward direction.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class DeleteForwardTextIntent extends Intent{
/// Creates an instance of DeleteForwardTextIntent.
const DeleteForwardTextIntent();
}
/// An [Intent] to delete a word in the forward direction.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class DeleteForwardByWordTextIntent extends Intent{
/// Creates an instance of DeleteByWordTextIntent.
const DeleteForwardByWordTextIntent();
}
/// An [Intent] to delete a line in the forward direction.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class DeleteForwardByLineTextIntent extends Intent{
/// Creates an instance of DeleteByLineTextIntent.
const DeleteForwardByLineTextIntent();
}
/// An [Intent] to send the event straight to the engine, but only if a
/// TextEditingTarget is focused.
///
......
......@@ -962,8 +962,7 @@ void main() {
expect(currentSelection.isCollapsed, true);
expect(currentSelection.baseOffset, 0);
await simulateKeyDownEvent(LogicalKeyboardKey.delete, platform: 'android');
await simulateKeyUpEvent(LogicalKeyboardKey.delete, platform: 'android');
editable.deleteForward(SelectionChangedCause.keyboard);
expect(delegate.textEditingValue.text, 'est');
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
......@@ -1013,8 +1012,7 @@ void main() {
expect(currentSelection.isCollapsed, true);
expect(currentSelection.baseOffset, 4);
await simulateKeyDownEvent(LogicalKeyboardKey.delete, platform: 'android');
await simulateKeyUpEvent(LogicalKeyboardKey.delete, platform: 'android');
editable.deleteForward(SelectionChangedCause.keyboard);
expect(delegate.textEditingValue.text, '01236789');
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
......@@ -1064,8 +1062,7 @@ void main() {
expect(currentSelection.isCollapsed, true);
expect(currentSelection.baseOffset, 4);
await simulateKeyDownEvent(LogicalKeyboardKey.delete, platform: 'android');
await simulateKeyUpEvent(LogicalKeyboardKey.delete, platform: 'android');
editable.deleteForward(SelectionChangedCause.keyboard);
expect(delegate.textEditingValue.text, '01232345');
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
......@@ -1114,8 +1111,7 @@ void main() {
expect(currentSelection.isCollapsed, true);
expect(currentSelection.baseOffset, 0);
await simulateKeyDownEvent(LogicalKeyboardKey.delete, platform: 'android');
await simulateKeyUpEvent(LogicalKeyboardKey.delete, platform: 'android');
editable.deleteForward(SelectionChangedCause.keyboard);
expect(delegate.textEditingValue.text, '');
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
......@@ -1170,8 +1166,7 @@ void main() {
expect(editable.selection?.isCollapsed, true);
expect(editable.selection?.baseOffset, 3);
await simulateKeyDownEvent(LogicalKeyboardKey.delete, platform: 'android');
await simulateKeyUpEvent(LogicalKeyboardKey.delete, platform: 'android');
editable.deleteForward(SelectionChangedCause.keyboard);
expect(delegate.textEditingValue.text, 'W Sczebrzeszynie chrząszcz brzmi w trzcinie');
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
......@@ -1401,7 +1396,7 @@ void main() {
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/58068
group('delete', () {
test('handles selection', () async {
test('when as a non-collapsed selection, it should delete a selection', () async {
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: 'test',
......@@ -1431,18 +1426,17 @@ void main() {
editable.hasFocus = true;
pumpFrame();
await simulateKeyDownEvent(LogicalKeyboardKey.delete, platform: 'android');
await simulateKeyUpEvent(LogicalKeyboardKey.delete, platform: 'android');
editable.delete(SelectionChangedCause.keyboard);
expect(delegate.textEditingValue.text, 'tt');
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, 1);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
test('is a no-op at the end of the text', () async {
test('when as simple text, it should delete the character to the left', () async {
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: 'test',
selection: TextSelection.collapsed(offset: 4),
selection: TextSelection.collapsed(offset: 3),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable(
......@@ -1461,25 +1455,132 @@ void main() {
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(offset: 4),
selection: const TextSelection.collapsed(offset: 3),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
await simulateKeyDownEvent(LogicalKeyboardKey.delete, platform: 'android');
await simulateKeyUpEvent(LogicalKeyboardKey.delete, platform: 'android');
expect(delegate.textEditingValue.text, 'test');
editable.delete(SelectionChangedCause.keyboard);
expect(delegate.textEditingValue.text, 'tet');
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, 2);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
test('when has surrogate pairs, it should delete the pair', () async {
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: '\u{1F44D}',
selection: TextSelection.collapsed(offset: 2),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey,
selectionColor: Colors.black,
textDirection: TextDirection.ltr,
cursorColor: Colors.red,
offset: viewportOffset,
textSelectionDelegate: delegate,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: '\u{1F44D}', // Thumbs up
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(offset: 2),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
editable.delete(SelectionChangedCause.keyboard);
expect(delegate.textEditingValue.text, '');
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, 0);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
test('when has grapheme clusters, it should delete the grapheme cluster', () async {
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: '0123👨‍👩‍👦2345',
selection: TextSelection.collapsed(offset: 12),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey,
selectionColor: Colors.black,
textDirection: TextDirection.ltr,
cursorColor: Colors.red,
offset: viewportOffset,
textSelectionDelegate: delegate,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: '0123👨‍👩‍👦2345',
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(offset: 12),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
editable.delete(SelectionChangedCause.keyboard);
expect(delegate.textEditingValue.text, '01232345');
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, 4);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
test('handles obscured text', () async {
test('when is at the start of the text, it should be a no-op', () async {
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: 'test',
selection: TextSelection.collapsed(offset: 0),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey,
selectionColor: Colors.black,
textDirection: TextDirection.ltr,
cursorColor: Colors.red,
offset: viewportOffset,
textSelectionDelegate: delegate,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: 'test',
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(offset: 0),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
editable.delete(SelectionChangedCause.keyboard);
expect(delegate.textEditingValue.text, 'test');
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, 0);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
test('when input has obscured text, it should delete the character to the left', () async {
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: 'test',
selection: TextSelection.collapsed(offset: 0),
selection: TextSelection.collapsed(offset: 4),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
......@@ -1500,28 +1601,104 @@ void main() {
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(offset: 0),
selection: const TextSelection.collapsed(offset: 4),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
await simulateKeyDownEvent(LogicalKeyboardKey.delete, platform: 'android');
await simulateKeyUpEvent(LogicalKeyboardKey.delete, platform: 'android');
editable.delete(SelectionChangedCause.keyboard);
expect(delegate.textEditingValue.text, 'tes');
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, 3);
}, skip: isBrowser);
expect(delegate.textEditingValue.text, 'est');
test('when using cjk characters', () async {
const String text = '用多個塊測試';
const int offset = 4;
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: offset),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey,
selectionColor: Colors.black,
textDirection: TextDirection.ltr,
cursorColor: Colors.red,
offset: viewportOffset,
textSelectionDelegate: delegate,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: text,
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(offset: offset),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
editable.delete(SelectionChangedCause.keyboard);
expect(delegate.textEditingValue.text, '用多個測試');
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, 3);
}, skip: isBrowser);
test('when using rtl', () async {
const String text = 'برنامج أهلا بالعالم';
const int offset = text.length;
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: offset),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey,
selectionColor: Colors.black,
textDirection: TextDirection.rtl,
cursorColor: Colors.red,
offset: viewportOffset,
textSelectionDelegate: delegate,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: text,
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(offset: offset),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
editable.delete(SelectionChangedCause.keyboard);
expect(delegate.textEditingValue.text, 'برنامج أهلا بالعال');
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, 0);
expect(delegate.textEditingValue.selection.baseOffset, text.length - 1);
}, skip: isBrowser);
});
group('backspace', () {
test('handles selection', () async {
group('deleteByWord', () {
test('when cursor is on the middle of a word, it should delete the left part of the word', () async {
const String text = 'test with multiple blocks';
const int offset = 8;
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: 'test',
selection: TextSelection(baseOffset: 1, extentOffset: 3),
text: text,
selection: TextSelection.collapsed(offset: offset),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable(
......@@ -1535,30 +1712,31 @@ void main() {
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: 'test',
text: text,
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection(baseOffset: 1, extentOffset: 3),
selection: const TextSelection.collapsed(offset: offset),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
await simulateKeyDownEvent(LogicalKeyboardKey.backspace, platform: 'android');
await simulateKeyUpEvent(LogicalKeyboardKey.backspace, platform: 'android');
expect(delegate.textEditingValue.text, 'tt');
editable.deleteByWord(SelectionChangedCause.keyboard, false);
expect(delegate.textEditingValue.text, 'test h multiple blocks');
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, 1);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
expect(delegate.textEditingValue.selection.baseOffset, 5);
}, skip: isBrowser);
test('handles simple text', () async {
test('when includeWhiteSpace is true, it should treat a whiteSpace as a single word', () async {
const String text = 'test with multiple blocks';
const int offset = 10;
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: 'test',
selection: TextSelection.collapsed(offset: 3),
text: text,
selection: TextSelection.collapsed(offset: offset),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable(
......@@ -1572,30 +1750,31 @@ void main() {
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: 'test',
text: text,
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(offset: 3),
selection: const TextSelection.collapsed(offset: offset),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
await simulateKeyDownEvent(LogicalKeyboardKey.backspace, platform: 'android');
await simulateKeyUpEvent(LogicalKeyboardKey.backspace, platform: 'android');
expect(delegate.textEditingValue.text, 'tet');
editable.deleteByWord(SelectionChangedCause.keyboard);
expect(delegate.textEditingValue.text, 'test withmultiple blocks');
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, 2);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
expect(delegate.textEditingValue.selection.baseOffset, 9);
}, skip: isBrowser);
test('handles surrogate pairs', () async {
test('when cursor is after a word, it should delete the whole word', () async {
const String text = 'test with multiple blocks';
const int offset = 9;
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: '\u{1F44D}',
selection: TextSelection.collapsed(offset: 2),
text: text,
selection: TextSelection.collapsed(offset: offset),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable(
......@@ -1609,30 +1788,31 @@ void main() {
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: '\u{1F44D}', // Thumbs up
text: text,
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(offset: 2),
selection: const TextSelection.collapsed(offset: offset),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
await simulateKeyDownEvent(LogicalKeyboardKey.backspace, platform: 'android');
await simulateKeyUpEvent(LogicalKeyboardKey.backspace, platform: 'android');
expect(delegate.textEditingValue.text, '');
editable.deleteByWord(SelectionChangedCause.keyboard, false);
expect(delegate.textEditingValue.text, 'test multiple blocks');
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, 0);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
expect(delegate.textEditingValue.selection.baseOffset, 5);
}, skip: isBrowser);
test('handles grapheme clusters', () async {
test('when cursor is preceeded by white spaces, it should delete the spaces and the next word to the left', () async {
const String text = 'test with multiple blocks';
const int offset = 12;
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: '0123👨‍👩‍👦2345',
selection: TextSelection.collapsed(offset: 12),
text: text,
selection: TextSelection.collapsed(offset: offset),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable(
......@@ -1646,30 +1826,31 @@ void main() {
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: '0123👨‍👩‍👦2345',
text: text,
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(offset: 12),
selection: const TextSelection.collapsed(offset: offset),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
await simulateKeyDownEvent(LogicalKeyboardKey.backspace, platform: 'android');
await simulateKeyUpEvent(LogicalKeyboardKey.backspace, platform: 'android');
expect(delegate.textEditingValue.text, '01232345');
editable.deleteByWord(SelectionChangedCause.keyboard, false);
expect(delegate.textEditingValue.text, 'test multiple blocks');
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, 4);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
expect(delegate.textEditingValue.selection.baseOffset, 5);
}, skip: isBrowser);
test('is a no-op at the start of the text', () async {
test('when cursor is preceeded by tabs spaces', () async {
const String text = 'test with\t\t\tmultiple blocks';
const int offset = 12;
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: 'test',
selection: TextSelection.collapsed(offset: 0),
text: text,
selection: TextSelection.collapsed(offset: offset),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable(
......@@ -1683,32 +1864,32 @@ void main() {
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: 'test',
text: text,
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(offset: 0),
selection: const TextSelection.collapsed(offset: offset),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
await simulateKeyDownEvent(LogicalKeyboardKey.backspace, platform: 'android');
await simulateKeyUpEvent(LogicalKeyboardKey.backspace, platform: 'android');
expect(delegate.textEditingValue.text, 'test');
editable.deleteByWord(SelectionChangedCause.keyboard, false);
expect(delegate.textEditingValue.text, 'test multiple blocks');
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, 0);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
expect(delegate.textEditingValue.selection.baseOffset, 5);
}, skip: isBrowser);
test('handles obscured text', () async {
test('when cursor is preceeded by break line, it should delete the breaking line and the word right before it', () async {
const String text = 'test with\nmultiple blocks';
const int offset = 10;
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: 'test',
selection: TextSelection.collapsed(offset: 4),
);
text: text,
selection: TextSelection.collapsed(offset: offset),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey,
......@@ -1717,29 +1898,1061 @@ void main() {
cursorColor: Colors.red,
offset: viewportOffset,
textSelectionDelegate: delegate,
obscureText: true,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: '****',
text: text,
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(offset: 4),
selection: const TextSelection.collapsed(offset: offset),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
await simulateKeyDownEvent(LogicalKeyboardKey.backspace, platform: 'android');
await simulateKeyUpEvent(LogicalKeyboardKey.backspace, platform: 'android');
editable.deleteByWord(SelectionChangedCause.keyboard, false);
expect(delegate.textEditingValue.text, 'test multiple blocks');
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, 5);
}, skip: isBrowser);
expect(delegate.textEditingValue.text, 'tes');
test('when using cjk characters', () async {
const String text = '用多個塊測試';
const int offset = 4;
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: offset),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey,
selectionColor: Colors.black,
textDirection: TextDirection.ltr,
cursorColor: Colors.red,
offset: viewportOffset,
textSelectionDelegate: delegate,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: text,
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(offset: offset),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
editable.deleteByWord(SelectionChangedCause.keyboard, false);
expect(delegate.textEditingValue.text, '用多個測試');
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, 3);
}, skip: isBrowser);
test('when using rtl', () async {
const String text = 'برنامج أهلا بالعالم';
const int offset = text.length;
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: offset),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey,
selectionColor: Colors.black,
textDirection: TextDirection.rtl,
cursorColor: Colors.red,
offset: viewportOffset,
textSelectionDelegate: delegate,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: text,
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(offset: offset),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
editable.deleteByWord(SelectionChangedCause.keyboard, false);
expect(delegate.textEditingValue.text, 'برنامج أهلا ');
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, 3);
expect(delegate.textEditingValue.selection.baseOffset, 12);
}, skip: isBrowser);
test('when input has obscured text, it should delete everything before the selection', () async {
const int offset = 21;
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: 'test with multiple\n\n words',
selection: TextSelection.collapsed(offset: offset),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey,
selectionColor: Colors.black,
textDirection: TextDirection.ltr,
cursorColor: Colors.red,
offset: viewportOffset,
textSelectionDelegate: delegate,
obscureText: true,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: '****',
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(offset: offset),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
editable.deleteByWord(SelectionChangedCause.keyboard, false);
expect(delegate.textEditingValue.text, 'words');
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, 0);
}, skip: isBrowser);
});
group('deleteByLine', () {
test('when cursor is on last character of a line, it should delete everything to the left', () async {
const String text = 'test with multiple blocks';
const int offset = text.length;
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: offset),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey,
selectionColor: Colors.black,
textDirection: TextDirection.ltr,
cursorColor: Colors.red,
offset: viewportOffset,
textSelectionDelegate: delegate,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: text,
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(offset: offset),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
editable.deleteByLine(SelectionChangedCause.keyboard);
expect(delegate.textEditingValue.text, '');
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, 0);
}, skip: isBrowser);
test('when cursor is on the middle of a word, it should delete delete everything to the left', () async {
const String text = 'test with multiple blocks';
const int offset = 8;
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: offset),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey,
selectionColor: Colors.black,
textDirection: TextDirection.ltr,
cursorColor: Colors.red,
offset: viewportOffset,
textSelectionDelegate: delegate,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: text,
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(offset: offset),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
editable.deleteByLine(SelectionChangedCause.keyboard);
expect(delegate.textEditingValue.text, 'h multiple blocks');
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, 0);
}, skip: isBrowser);
test('when previous character is a breakline, it should preserve it', () async {
const String text = 'test with\nmultiple blocks';
const int offset = 10;
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: offset),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey,
selectionColor: Colors.black,
textDirection: TextDirection.ltr,
cursorColor: Colors.red,
offset: viewportOffset,
textSelectionDelegate: delegate,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: text,
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(offset: offset),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
editable.deleteByLine(SelectionChangedCause.keyboard);
expect(delegate.textEditingValue.text, text);
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, offset);
}, skip: isBrowser);
test('when text is multiline, it should delete until the first line break it finds', () async {
const String text = 'test with\n\nMore stuff right here.\nmultiple blocks';
const int offset = 22;
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: offset),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey,
selectionColor: Colors.black,
textDirection: TextDirection.ltr,
cursorColor: Colors.red,
offset: viewportOffset,
textSelectionDelegate: delegate,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: text,
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(offset: offset),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
editable.deleteByLine(SelectionChangedCause.keyboard);
expect(delegate.textEditingValue.text, 'test with\n\nright here.\nmultiple blocks');
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, 11);
}, skip: isBrowser);
test('when input has obscured text, it should delete everything before the selection', () async {
const int offset = 21;
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: 'test with multiple\n\n words',
selection: TextSelection.collapsed(offset: offset),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey,
selectionColor: Colors.black,
textDirection: TextDirection.ltr,
cursorColor: Colors.red,
offset: viewportOffset,
textSelectionDelegate: delegate,
obscureText: true,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: '****',
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(offset: offset),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
editable.deleteByLine(SelectionChangedCause.keyboard);
expect(delegate.textEditingValue.text, 'words');
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, 0);
}, skip: isBrowser);
});
group('deleteForward', () {
test('when as a non-collapsed selection, it should delete a selection', () async {
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: 'test',
selection: TextSelection(baseOffset: 1, extentOffset: 3),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey,
selectionColor: Colors.black,
textDirection: TextDirection.ltr,
cursorColor: Colors.red,
offset: viewportOffset,
textSelectionDelegate: delegate,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: 'test',
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection(baseOffset: 1, extentOffset: 3),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
editable.deleteForward(SelectionChangedCause.keyboard);
expect(delegate.textEditingValue.text, 'tt');
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, 1);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
test('when includeWhiteSpace is true, it should treat a whiteSpace as a single word', () async {
const String text = 'test with multiple blocks';
const int offset = 9;
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: offset),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey,
selectionColor: Colors.black,
textDirection: TextDirection.ltr,
cursorColor: Colors.red,
offset: viewportOffset,
textSelectionDelegate: delegate,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: text,
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(offset: offset),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
editable.deleteForwardByWord(SelectionChangedCause.keyboard);
expect(delegate.textEditingValue.text, 'test withmultiple blocks');
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, 9);
}, skip: isBrowser);
test('when at the end of a text, it should be a no-op', () async {
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: 'test',
selection: TextSelection.collapsed(offset: 4),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey,
selectionColor: Colors.black,
textDirection: TextDirection.ltr,
cursorColor: Colors.red,
offset: viewportOffset,
textSelectionDelegate: delegate,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: 'test',
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(offset: 4),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
editable.deleteForward(SelectionChangedCause.keyboard);
expect(delegate.textEditingValue.text, 'test');
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, 4);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
test('when the input has obscured text, it should delete the forward character', () async {
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: 'test',
selection: TextSelection.collapsed(offset: 0),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey,
selectionColor: Colors.black,
textDirection: TextDirection.ltr,
cursorColor: Colors.red,
offset: viewportOffset,
textSelectionDelegate: delegate,
obscureText: true,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: '****',
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(offset: 0),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
editable.deleteForward(SelectionChangedCause.keyboard);
expect(delegate.textEditingValue.text, 'est');
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, 0);
}, skip: isBrowser);
test('when using cjk characters', () async {
const String text = '用多個塊測試';
const int offset = 0;
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: offset),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey,
selectionColor: Colors.black,
textDirection: TextDirection.ltr,
cursorColor: Colors.red,
offset: viewportOffset,
textSelectionDelegate: delegate,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: text,
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(offset: offset),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
editable.deleteForward(SelectionChangedCause.keyboard);
expect(delegate.textEditingValue.text, '多個塊測試');
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, 0);
}, skip: isBrowser);
test('when using rtl', () async {
const String text = 'برنامج أهلا بالعالم';
const int offset = 0;
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: offset),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey,
selectionColor: Colors.black,
textDirection: TextDirection.rtl,
cursorColor: Colors.red,
offset: viewportOffset,
textSelectionDelegate: delegate,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: text,
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(offset: offset),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
editable.deleteForward(SelectionChangedCause.keyboard);
expect(delegate.textEditingValue.text, 'رنامج أهلا بالعالم');
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, 0);
}, skip: isBrowser);
});
group('deleteForwardByWord', () {
test('when cursor is on the middle of a word, it should delete the next part of the word', () async {
const String text = 'test with multiple blocks';
const int offset = 6;
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: offset),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey,
selectionColor: Colors.black,
textDirection: TextDirection.ltr,
cursorColor: Colors.red,
offset: viewportOffset,
textSelectionDelegate: delegate,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: text,
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(offset: offset),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
editable.deleteForwardByWord(SelectionChangedCause.keyboard, false);
expect(delegate.textEditingValue.text, 'test w multiple blocks');
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, offset);
}, skip: isBrowser);
test('when cursor is before a word, it should delete the whole word', () async {
const String text = 'test with multiple blocks';
const int offset = 10;
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: offset),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey,
selectionColor: Colors.black,
textDirection: TextDirection.ltr,
cursorColor: Colors.red,
offset: viewportOffset,
textSelectionDelegate: delegate,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: text,
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(offset: offset),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
editable.deleteForwardByWord(SelectionChangedCause.keyboard, false);
expect(delegate.textEditingValue.text, 'test with blocks');
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, offset);
}, skip: isBrowser);
test('when cursor is preceeded by white spaces, it should delete the spaces and the next word', () async {
const String text = 'test with multiple blocks';
const int offset = 9;
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: offset),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey,
selectionColor: Colors.black,
textDirection: TextDirection.ltr,
cursorColor: Colors.red,
offset: viewportOffset,
textSelectionDelegate: delegate,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: text,
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(offset: offset),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
editable.deleteForwardByWord(SelectionChangedCause.keyboard, false);
expect(delegate.textEditingValue.text, 'test with blocks');
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, offset);
}, skip: isBrowser);
test('when cursor is before tabs, it should delete the tabs and the next word', () async {
const String text = 'test with\t\t\tmultiple blocks';
const int offset = 9;
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: offset),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey,
selectionColor: Colors.black,
textDirection: TextDirection.ltr,
cursorColor: Colors.red,
offset: viewportOffset,
textSelectionDelegate: delegate,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: text,
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(offset: offset),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
editable.deleteForwardByWord(SelectionChangedCause.keyboard, false);
expect(delegate.textEditingValue.text, 'test with blocks');
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, offset);
}, skip: isBrowser);
test('when cursor is followed by break line, it should delete the next word', () async {
const String text = 'test with\n\n\nmultiple blocks';
const int offset = 9;
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: offset),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey,
selectionColor: Colors.black,
textDirection: TextDirection.ltr,
cursorColor: Colors.red,
offset: viewportOffset,
textSelectionDelegate: delegate,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: text,
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(offset: offset),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
editable.deleteForwardByWord(SelectionChangedCause.keyboard, false);
expect(delegate.textEditingValue.text, 'test with blocks');
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, offset);
}, skip: isBrowser);
test('when using cjk characters', () async {
const String text = '用多個塊測試';
const int offset = 0;
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: offset),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey,
selectionColor: Colors.black,
textDirection: TextDirection.ltr,
cursorColor: Colors.red,
offset: viewportOffset,
textSelectionDelegate: delegate,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: text,
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(offset: offset),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
editable.deleteForwardByWord(SelectionChangedCause.keyboard, false);
expect(delegate.textEditingValue.text, '多個塊測試');
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, offset);
}, skip: isBrowser);
test('when using rtl', () async {
const String text = 'برنامج أهلا بالعالم';
const int offset = 0;
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: offset),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey,
selectionColor: Colors.black,
textDirection: TextDirection.rtl,
cursorColor: Colors.red,
offset: viewportOffset,
textSelectionDelegate: delegate,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: text,
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(offset: offset),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
editable.deleteForwardByWord(SelectionChangedCause.keyboard, false);
expect(delegate.textEditingValue.text, ' أهلا بالعالم');
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, offset);
}, skip: isBrowser);
test('when input has obscured text, it should delete everything after the selection', () async {
const int offset = 4;
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: 'test with multiple\n\n words',
selection: TextSelection.collapsed(offset: offset),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey,
selectionColor: Colors.black,
textDirection: TextDirection.ltr,
cursorColor: Colors.red,
offset: viewportOffset,
textSelectionDelegate: delegate,
obscureText: true,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: '****',
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(offset: offset),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
editable.deleteForwardByWord(SelectionChangedCause.keyboard, false);
expect(delegate.textEditingValue.text, 'test');
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, offset);
}, skip: isBrowser);
});
group('deleteForwardByLine', () {
test('when cursor is on first character of a line, it should delete everything that follows', () async {
const String text = 'test with multiple blocks';
const int offset = 4;
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: offset),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey,
selectionColor: Colors.black,
textDirection: TextDirection.ltr,
cursorColor: Colors.red,
offset: viewportOffset,
textSelectionDelegate: delegate,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: text,
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(offset: offset),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
editable.deleteForwardByLine(SelectionChangedCause.keyboard);
expect(delegate.textEditingValue.text, 'test');
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, offset);
}, skip: isBrowser);
test('when cursor is on the middle of a word, it should delete delete everything that follows', () async {
const String text = 'test with multiple blocks';
const int offset = 8;
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: offset),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey,
selectionColor: Colors.black,
textDirection: TextDirection.ltr,
cursorColor: Colors.red,
offset: viewportOffset,
textSelectionDelegate: delegate,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: text,
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(offset: offset),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
editable.deleteForwardByLine(SelectionChangedCause.keyboard);
expect(delegate.textEditingValue.text, 'test wit');
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, offset);
}, skip: isBrowser);
test('when next character is a breakline, it should preserve it', () async {
const String text = 'test with\n\n\nmultiple blocks';
const int offset = 9;
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: offset),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey,
selectionColor: Colors.black,
textDirection: TextDirection.ltr,
cursorColor: Colors.red,
offset: viewportOffset,
textSelectionDelegate: delegate,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: text,
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(offset: offset),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
editable.deleteForwardByLine(SelectionChangedCause.keyboard);
expect(delegate.textEditingValue.text, text);
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, offset);
}, skip: isBrowser);
test('when text is multiline, it should delete until the first line break it finds', () async {
const String text = 'test with\n\nMore stuff right here.\nmultiple blocks';
const int offset = 2;
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: offset),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey,
selectionColor: Colors.black,
textDirection: TextDirection.ltr,
cursorColor: Colors.red,
offset: viewportOffset,
textSelectionDelegate: delegate,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: text,
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(offset: offset),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
editable.deleteForwardByLine(SelectionChangedCause.keyboard);
expect(delegate.textEditingValue.text, 'te\n\nMore stuff right here.\nmultiple blocks');
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, offset);
}, skip: isBrowser);
test('when input has obscured text, it should delete everything after the selection', () async {
const int offset = 4;
final TextSelectionDelegate delegate = FakeEditableTextState()
..textEditingValue = const TextEditingValue(
text: 'test with multiple\n\n words',
selection: TextSelection.collapsed(offset: offset),
);
final ViewportOffset viewportOffset = ViewportOffset.zero();
final RenderEditable editable = RenderEditable(
backgroundCursorColor: Colors.grey,
selectionColor: Colors.black,
textDirection: TextDirection.ltr,
cursorColor: Colors.red,
offset: viewportOffset,
textSelectionDelegate: delegate,
obscureText: true,
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
text: '****',
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
selection: const TextSelection.collapsed(offset: offset),
);
layout(editable);
editable.hasFocus = true;
pumpFrame();
editable.deleteForwardByLine(SelectionChangedCause.keyboard);
expect(delegate.textEditingValue.text, 'test');
expect(delegate.textEditingValue.selection.isCollapsed, true);
expect(delegate.textEditingValue.selection.baseOffset, offset);
}, skip: isBrowser);
});
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment