Revert "Support Scribble Handwriting" (#96615)

This commit is contained in:
LongCatIsLooong 2022-01-13 12:00:19 -08:00 committed by GitHub
parent 4fda08efa9
commit 9c23106711
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 53 additions and 1249 deletions

View file

@ -89,4 +89,3 @@ Pradumna Saraf <pradumnasaraf@gmail.com>
Kai Yu <yk3372@gmail.com>
Denis Grafov <grafov.denis@gmail.com>
TheOneWithTheBraid <the-one@with-the-braid.cf>
Twin Sun, LLC <google-contrib@twinsunsolutions.com>

View file

@ -297,7 +297,6 @@ class CupertinoTextField extends StatefulWidget {
this.autofillHints = const <String>[],
this.clipBehavior = Clip.hardEdge,
this.restorationId,
this.scribbleEnabled = true,
this.enableIMEPersonalizedLearning = true,
}) : assert(textAlign != null),
assert(readOnly != null),
@ -455,7 +454,6 @@ class CupertinoTextField extends StatefulWidget {
this.autofillHints = const <String>[],
this.clipBehavior = Clip.hardEdge,
this.restorationId,
this.scribbleEnabled = true,
this.enableIMEPersonalizedLearning = true,
}) : assert(textAlign != null),
assert(readOnly != null),
@ -800,9 +798,6 @@ class CupertinoTextField extends StatefulWidget {
/// {@macro flutter.material.textfield.restorationId}
final String? restorationId;
/// {@macro flutter.widgets.editableText.scribbleEnabled}
final bool scribbleEnabled;
/// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
final bool enableIMEPersonalizedLearning;
@ -848,7 +843,6 @@ class CupertinoTextField extends StatefulWidget {
properties.add(DiagnosticsProperty<TextAlignVertical>('textAlignVertical', textAlignVertical, defaultValue: null));
properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
properties.add(DiagnosticsProperty<Clip>('clipBehavior', clipBehavior, defaultValue: Clip.hardEdge));
properties.add(DiagnosticsProperty<bool>('scribbleEnabled', scribbleEnabled, defaultValue: true));
properties.add(DiagnosticsProperty<bool>('enableIMEPersonalizedLearning', enableIMEPersonalizedLearning, defaultValue: true));
}
}
@ -969,9 +963,6 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
if (cause == SelectionChangedCause.keyboard)
return false;
if (cause == SelectionChangedCause.scribble)
return true;
if (_effectiveController.text.isNotEmpty)
return true;
@ -1301,7 +1292,6 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
autofillClient: this,
clipBehavior: widget.clipBehavior,
restorationId: 'editable',
scribbleEnabled: widget.scribbleEnabled,
enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
),
),

View file

@ -329,7 +329,6 @@ class TextField extends StatefulWidget {
this.autofillHints = const <String>[],
this.clipBehavior = Clip.hardEdge,
this.restorationId,
this.scribbleEnabled = true,
this.enableIMEPersonalizedLearning = true,
}) : assert(textAlign != null),
assert(readOnly != null),
@ -769,9 +768,6 @@ class TextField extends StatefulWidget {
/// {@endtemplate}
final String? restorationId;
/// {@macro flutter.widgets.editableText.scribbleEnabled}
final bool scribbleEnabled;
/// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
final bool enableIMEPersonalizedLearning;
@ -816,7 +812,6 @@ class TextField extends StatefulWidget {
properties.add(DiagnosticsProperty<ScrollController>('scrollController', scrollController, defaultValue: null));
properties.add(DiagnosticsProperty<ScrollPhysics>('scrollPhysics', scrollPhysics, defaultValue: null));
properties.add(DiagnosticsProperty<Clip>('clipBehavior', clipBehavior, defaultValue: Clip.hardEdge));
properties.add(DiagnosticsProperty<bool>('scribbleEnabled', scribbleEnabled, defaultValue: true));
properties.add(DiagnosticsProperty<bool>('enableIMEPersonalizedLearning', enableIMEPersonalizedLearning, defaultValue: true));
}
}
@ -1034,7 +1029,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
if (!_isEnabled)
return false;
if (cause == SelectionChangedCause.longPress || cause == SelectionChangedCause.scribble)
if (cause == SelectionChangedCause.longPress)
return true;
if (_effectiveController.text.isNotEmpty)
@ -1278,7 +1273,6 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
autocorrectionTextRectColor: autocorrectionTextRectColor,
clipBehavior: widget.clipBehavior,
restorationId: 'editable',
scribbleEnabled: widget.scribbleEnabled,
enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
),
),

View file

@ -1265,16 +1265,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
// [assembleSemanticsNode] invocations.
Queue<SemanticsNode>? _cachedChildNodes;
/// Returns a list of rects that bound the given selection.
///
/// See [TextPainter.getBoxesForSelection] for more details.
List<Rect> getBoxesForSelection(TextSelection selection) {
_computeTextMetricsIfNeeded();
return _textPainter.getBoxesForSelection(selection)
.map((TextBox textBox) => textBox.toRect().shift(_paintOffset))
.toList();
}
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);

View file

@ -955,9 +955,6 @@ enum SelectionChangedCause {
/// The user used the mouse to change the selection by dragging over a piece
/// of text.
drag,
/// The user used iPadOS 14+ Scribble to change the selection.
scribble,
}
/// A mixin for manipulating the selection, provided for toolbar or shortcut
@ -1108,76 +1105,6 @@ abstract class TextInputClient {
///
/// [TextInputClient] should cleanup its connection and finalize editing.
void connectionClosed();
/// Requests that the client show the editing toolbar, for example when the
/// platform changes the selection through a non-flutter method such as
/// scribble.
void showToolbar() {}
/// Requests that the client add a text placeholder to reserve visual space
/// in the text.
///
/// For example, this is called when responding to UIKit requesting
/// a text placeholder be added at the current selection, such as when
/// requesting additional writing space with iPadOS14 Scribble.
void insertTextPlaceholder(Size size) {}
/// Requests that the client remove the text placeholder.
void removeTextPlaceholder() {}
}
/// An interface to receive focus from the engine.
///
/// This is currently only used to handle UIIndirectScribbleInteraction.
abstract class ScribbleClient {
/// A unique identifier for this element.
String get elementIdentifier;
/// Called by the engine when the [ScribbleClient] should receive focus.
///
/// For example, this method is called during a UIIndirectScribbleInteraction.
void onScribbleFocus(Offset offset);
/// Tests whether the [ScribbleClient] overlaps the given rectangle bounds.
bool isInScribbleRect(Rect rect);
/// The current bounds of the [ScribbleClient].
Rect get bounds;
}
/// Represents a selection rect for a character and it's position in the text.
///
/// This is used to report the current text selection rect and position data
/// to the engine for Scribble support on iPadOS 14.
@immutable
class SelectionRect {
/// Constructor for creating a [SelectionRect] from a text [position] and
/// [bounds].
const SelectionRect({required this.position, required this.bounds});
/// The position of this selection rect within the text String.
final int position;
/// The rectangle representing the bounds of this selection rect within the
/// currently focused [RenderEditable]'s coordinate space.
final Rect bounds;
@override
bool operator ==(Object other) {
if (identical(this, other))
return true;
if (runtimeType != other.runtimeType)
return false;
return other is SelectionRect
&& other.position == position
&& other.bounds == bounds;
}
@override
int get hashCode => hashValues(position, bounds);
@override
String toString() => 'SelectionRect($position, $bounds)';
}
/// An interface to receive granular information from [TextInput].
@ -1227,7 +1154,6 @@ class TextInputConnection {
Matrix4? _cachedTransform;
Rect? _cachedRect;
Rect? _cachedCaretRect;
List<SelectionRect> _cachedSelectionRects = <SelectionRect>[];
static int _nextId = 1;
final int _id;
@ -1250,12 +1176,6 @@ class TextInputConnection {
/// Whether this connection is currently interacting with the text input control.
bool get attached => TextInput._instance._currentConnection == this;
/// Whether there is currently a Scribble interaction in progress.
///
/// This is used to make sure selection handles are shown when UIKit changes
/// the selection during a Scribble interaction.
bool get scribbleInProgress => TextInput._instance.scribbleInProgress;
/// Requests that the text input control become visible.
void show() {
assert(attached);
@ -1354,19 +1274,6 @@ class TextInputConnection {
);
}
/// Send the bounding boxes of the current selected glyphs in the client to
/// the platform's text input plugin.
///
/// These are used by the engine during a UIDirectScribbleInteraction.
void setSelectionRects(List<SelectionRect> selectionRects) {
if (!listEquals(_cachedSelectionRects, selectionRects)) {
_cachedSelectionRects = selectionRects;
TextInput._instance._setSelectionRects(selectionRects.map((SelectionRect rect) {
return <num>[rect.bounds.left, rect.bounds.top, rect.bounds.width, rect.bounds.height, rect.position];
}).toList());
}
}
/// Send text styling information.
///
/// This information is used by the Flutter Web Engine to change the style
@ -1628,43 +1535,10 @@ class TextInput {
TextInputConnection? _currentConnection;
late TextInputConfiguration _currentConfiguration;
final Map<String, ScribbleClient> _scribbleClients = <String, ScribbleClient>{};
bool _scribbleInProgress = false;
/// Used for testing within the Flutter SDK to get the currently registered [ScribbleClient] list.
@visibleForTesting
static Map<String, ScribbleClient> get scribbleClients => TextInput._instance._scribbleClients;
/// Returns true if a scribble interaction is currently happening.
bool get scribbleInProgress => _scribbleInProgress;
Future<dynamic> _handleTextInputInvocation(MethodCall methodCall) async {
final String method = methodCall.method;
if (method == 'TextInputClient.focusElement') {
final List<dynamic> args = methodCall.arguments as List<dynamic>;
_scribbleClients[args[0]]?.onScribbleFocus(Offset((args[1] as num).toDouble(), (args[2] as num).toDouble()));
return;
} else if (method == 'TextInputClient.requestElementsInRect') {
final List<double> args = (methodCall.arguments as List<dynamic>).cast<num>().map<double>((num value) => value.toDouble()).toList();
return _scribbleClients.keys.where((String elementIdentifier) {
final Rect rect = Rect.fromLTWH(args[0], args[1], args[2], args[3]);
if (!(_scribbleClients[elementIdentifier]?.isInScribbleRect(rect) ?? false))
return false;
final Rect bounds = _scribbleClients[elementIdentifier]?.bounds ?? Rect.zero;
return !(bounds == Rect.zero || bounds.hasNaN || bounds.isInfinite);
}).map((String elementIdentifier) {
final Rect bounds = _scribbleClients[elementIdentifier]!.bounds;
return <dynamic>[elementIdentifier, ...<dynamic>[bounds.left, bounds.top, bounds.width, bounds.height]];
}).toList();
} else if (method == 'TextInputClient.scribbleInteractionBegan') {
_scribbleInProgress = true;
return;
} else if (method == 'TextInputClient.scribbleInteractionFinished') {
_scribbleInProgress = false;
return;
}
if (_currentConnection == null)
return;
final String method = methodCall.method;
// The requestExistingInputState request needs to be handled regardless of
// the client ID, as long as we have a _currentConnection.
@ -1756,15 +1630,6 @@ class TextInput {
case 'TextInputClient.showAutocorrectionPromptRect':
_currentConnection!._client.showAutocorrectionPromptRect(args[1] as int, args[2] as int);
break;
case 'TextInputClient.showToolbar':
_currentConnection!._client.showToolbar();
break;
case 'TextInputClient.insertTextPlaceholder':
_currentConnection!._client.insertTextPlaceholder(Size((args[1] as num).toDouble(), (args[2] as num).toDouble()));
break;
case 'TextInputClient.removeTextPlaceholder':
_currentConnection!._client.removeTextPlaceholder();
break;
default:
throw MissingPluginException();
}
@ -1838,13 +1703,6 @@ class TextInput {
);
}
void _setSelectionRects(List<List<num>> args) {
_channel.invokeMethod<void>(
'TextInput.setSelectionRects',
args,
);
}
void _setStyle(Map<String, dynamic> args) {
_channel.invokeMethod<void>(
'TextInput.setStyle',
@ -1907,18 +1765,4 @@ class TextInput {
shouldSave,
);
}
/// Registers a [ScribbleClient] with [elementIdentifier] that can be focused
/// by the engine.
///
/// For example, the registered [ScribbleClient] list is used to respond to
/// UIIndirectScribbleInteraction on an iPad.
static void registerScribbleElement(String elementIdentifier, ScribbleClient scribbleClient) {
TextInput._instance._scribbleClients[elementIdentifier] = scribbleClient;
}
/// Unregisters a [ScribbleClient] with [elementIdentifier].
static void unregisterScribbleElement(String elementIdentifier) {
TextInput._instance._scribbleClients.remove(elementIdentifier);
}
}

View file

@ -6,7 +6,7 @@ import 'dart:async';
import 'dart:math' as math;
import 'dart:ui' as ui hide TextStyle;
import 'package:characters/characters.dart' show CharacterRange, StringCharacters;
import 'package:characters/characters.dart' show CharacterRange;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart' show DragStartBehavior;
import 'package:flutter/rendering.dart';
@ -58,10 +58,6 @@ const Duration _kCursorBlinkWaitForStart = Duration(milliseconds: 150);
// is shown in an obscured text field.
const int _kObscureShowLatestCharCursorTicks = 3;
// The minimum width of an iPad screen. The smallest iPad is currently the
// iPad Mini 6th Gen according to ios-resolution.com.
const double _kIPadWidth = 1488.0;
/// A controller for an editable text field.
///
/// Whenever the user modifies a text field with an associated
@ -527,7 +523,6 @@ class EditableText extends StatefulWidget {
this.clipBehavior = Clip.hardEdge,
this.restorationId,
this.scrollBehavior,
this.scribbleEnabled = true,
this.enableIMEPersonalizedLearning = true,
}) : assert(controller != null),
assert(focusNode != null),
@ -1215,15 +1210,6 @@ class EditableText extends StatefulWidget {
/// [scrollPhysics].
final ScrollPhysics? scrollPhysics;
/// {@template flutter.widgets.editableText.scribbleEnabled}
/// Whether iOS 14 Scribble features are enabled for this widget.
///
/// Only available on iPads.
///
/// Defaults to true.
/// {@endtemplate}
final bool scribbleEnabled;
/// {@template flutter.widgets.editableText.selectionEnabled}
/// Same as [enableInteractiveSelection].
///
@ -1524,7 +1510,6 @@ class EditableText extends StatefulWidget {
properties.add(DiagnosticsProperty<ScrollPhysics>('scrollPhysics', scrollPhysics, defaultValue: null));
properties.add(DiagnosticsProperty<Iterable<String>>('autofillHints', autofillHints, defaultValue: null));
properties.add(DiagnosticsProperty<TextHeightBehavior>('textHeightBehavior', textHeightBehavior, defaultValue: null));
properties.add(DiagnosticsProperty<bool>('scribbleEnabled', scribbleEnabled, defaultValue: true));
properties.add(DiagnosticsProperty<bool>('enableIMEPersonalizedLearning', enableIMEPersonalizedLearning, defaultValue: true));
}
}
@ -1888,7 +1873,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
if (value.text == _value.text && value.composing == _value.composing) {
// `selection` is the only change.
_handleSelectionChanged(value.selection, (_textInputConnection?.scribbleInProgress ?? false) ? SelectionChangedCause.scribble : SelectionChangedCause.keyboard);
_handleSelectionChanged(value.selection, SelectionChangedCause.keyboard);
} else {
hideToolbar();
_currentPromptRectRange = null;
@ -2671,11 +2656,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
// Place cursor at the end if the selection is invalid when we receive focus.
_handleSelectionChanged(TextSelection.collapsed(offset: _value.text.length), null);
}
_cachedText = '';
_cachedFirstRect = null;
_cachedSize = Size.zero;
_cachedPlaceholder = -1;
} else {
WidgetsBinding.instance!.removeObserver(this);
setState(() { _currentPromptRectRange = null; });
@ -2683,78 +2663,13 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
updateKeepAlive();
}
String _cachedText = '';
Rect? _cachedFirstRect;
Size _cachedSize = Size.zero;
int _cachedPlaceholder = -1;
TextStyle? _cachedTextStyle;
void _updateSelectionRects({bool force = false}) {
if (!widget.scribbleEnabled)
return;
if (defaultTargetPlatform != TargetPlatform.iOS)
return;
// This is to avoid sending selection rects on non-iPad devices.
if (WidgetsBinding.instance!.window.physicalSize.shortestSide < _kIPadWidth)
return;
final String text = renderEditable.text?.toPlainText(includeSemanticsLabels: false, includePlaceholders: false) ?? '';
final List<Rect> firstSelectionBoxes = renderEditable.getBoxesForSelection(const TextSelection(baseOffset: 0, extentOffset: 1));
final Rect? firstRect = firstSelectionBoxes.isNotEmpty ? firstSelectionBoxes.first : null;
final ScrollDirection scrollDirection = _scrollController.position.userScrollDirection;
final Size size = renderEditable.size;
final bool textChanged = text != _cachedText;
final bool textStyleChanged = _cachedTextStyle != widget.style;
final bool firstRectChanged = _cachedFirstRect != firstRect;
final bool sizeChanged = _cachedSize != size;
final bool placeholderChanged = _cachedPlaceholder != _placeholderLocation;
if (scrollDirection == ScrollDirection.idle && (force || textChanged || textStyleChanged || firstRectChanged || sizeChanged || placeholderChanged)) {
_cachedText = text;
_cachedFirstRect = firstRect;
_cachedTextStyle = widget.style;
_cachedSize = size;
_cachedPlaceholder = _placeholderLocation;
bool belowRenderEditableBottom = false;
final List<SelectionRect> rects = List<SelectionRect?>.generate(
_cachedText.characters.length,
(int i) {
if (belowRenderEditableBottom)
return null;
final int offset = _cachedText.characters.getRange(0, i).string.length;
final SelectionRect selectionRect = SelectionRect(
bounds: renderEditable.getBoxesForSelection(TextSelection(baseOffset: offset, extentOffset: offset + _cachedText.characters.characterAt(i).string.length)).first,
position: offset,
);
if (renderEditable.paintBounds.bottom < selectionRect.bounds.top) {
belowRenderEditableBottom = true;
return null;
}
return selectionRect;
},
).where((SelectionRect? selectionRect) {
if (selectionRect == null)
return false;
if (renderEditable.paintBounds.right < selectionRect.bounds.left || selectionRect.bounds.right < renderEditable.paintBounds.left)
return false;
if (renderEditable.paintBounds.bottom < selectionRect.bounds.top || selectionRect.bounds.bottom < renderEditable.paintBounds.top)
return false;
return true;
}).map<SelectionRect>((SelectionRect? selectionRect) => selectionRect!).toList();
_textInputConnection!.setSelectionRects(rects);
}
}
void _updateSizeAndTransform() {
if (_hasInputConnection) {
final Size size = renderEditable.size;
final Matrix4 transform = renderEditable.getTransformTo(null);
_textInputConnection!.setEditableSizeAndTransform(size, transform);
_updateSelectionRects();
SchedulerBinding.instance!
.addPostFrameCallback((Duration _) => _updateSizeAndTransform());
} else if (_placeholderLocation != -1) {
removeTextPlaceholder();
}
}
@ -2837,7 +2752,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
///
/// Returns `false` if a toolbar couldn't be shown, such as when the toolbar
/// is already shown, or when no text selection currently exists.
@override
bool showToolbar() {
// Web is using native dom elements to enable clipboard functionality of the
// toolbar: copy, paste, select, cut. It might also provide additional
@ -2876,36 +2790,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
}
}
// Tracks the location a [_ScribblePlaceholder] should be rendered in the
// text.
//
// A value of -1 indicates there should be no placeholder, otherwise the
// value should be between 0 and the length of the text, inclusive.
int _placeholderLocation = -1;
@override
void insertTextPlaceholder(Size size) {
if (!widget.scribbleEnabled)
return;
if (!widget.controller.selection.isValid)
return;
setState(() {
_placeholderLocation = _value.text.length - widget.controller.selection.end;
});
}
@override
void removeTextPlaceholder() {
if (!widget.scribbleEnabled)
return;
setState(() {
_placeholderLocation = -1;
});
}
@override
String get autofillId => 'EditableText-$hashCode';
@ -3107,62 +2991,53 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
onCopy: _semanticsOnCopy(controls),
onCut: _semanticsOnCut(controls),
onPaste: _semanticsOnPaste(controls),
child: _ScribbleFocusable(
focusNode: widget.focusNode,
editableKey: _editableKey,
enabled: widget.scribbleEnabled,
updateSelectionRects: () {
_openInputConnection();
_updateSelectionRects(force: true);
},
child: _Editable(
key: _editableKey,
startHandleLayerLink: _startHandleLayerLink,
endHandleLayerLink: _endHandleLayerLink,
inlineSpan: buildTextSpan(),
value: _value,
cursorColor: _cursorColor,
backgroundCursorColor: widget.backgroundCursorColor,
showCursor: EditableText.debugDeterministicCursor
? ValueNotifier<bool>(widget.showCursor)
: _cursorVisibilityNotifier,
forceLine: widget.forceLine,
readOnly: widget.readOnly,
hasFocus: _hasFocus,
maxLines: widget.maxLines,
minLines: widget.minLines,
expands: widget.expands,
strutStyle: widget.strutStyle,
selectionColor: widget.selectionColor,
textScaleFactor: widget.textScaleFactor ?? MediaQuery.textScaleFactorOf(context),
textAlign: widget.textAlign,
textDirection: _textDirection,
locale: widget.locale,
textHeightBehavior: widget.textHeightBehavior ?? DefaultTextHeightBehavior.of(context),
textWidthBasis: widget.textWidthBasis,
obscuringCharacter: widget.obscuringCharacter,
obscureText: widget.obscureText,
autocorrect: widget.autocorrect,
smartDashesType: widget.smartDashesType,
smartQuotesType: widget.smartQuotesType,
enableSuggestions: widget.enableSuggestions,
offset: offset,
onCaretChanged: _handleCaretChanged,
rendererIgnoresPointer: widget.rendererIgnoresPointer,
cursorWidth: widget.cursorWidth,
cursorHeight: widget.cursorHeight,
cursorRadius: widget.cursorRadius,
cursorOffset: widget.cursorOffset ?? Offset.zero,
selectionHeightStyle: widget.selectionHeightStyle,
selectionWidthStyle: widget.selectionWidthStyle,
paintCursorAboveText: widget.paintCursorAboveText,
enableInteractiveSelection: widget.enableInteractiveSelection,
textSelectionDelegate: this,
devicePixelRatio: _devicePixelRatio,
promptRectRange: _currentPromptRectRange,
promptRectColor: widget.autocorrectionTextRectColor,
clipBehavior: widget.clipBehavior,
),
child: _Editable(
key: _editableKey,
startHandleLayerLink: _startHandleLayerLink,
endHandleLayerLink: _endHandleLayerLink,
inlineSpan: buildTextSpan(),
value: _value,
cursorColor: _cursorColor,
backgroundCursorColor: widget.backgroundCursorColor,
showCursor: EditableText.debugDeterministicCursor
? ValueNotifier<bool>(widget.showCursor)
: _cursorVisibilityNotifier,
forceLine: widget.forceLine,
readOnly: widget.readOnly,
hasFocus: _hasFocus,
maxLines: widget.maxLines,
minLines: widget.minLines,
expands: widget.expands,
strutStyle: widget.strutStyle,
selectionColor: widget.selectionColor,
textScaleFactor: widget.textScaleFactor ?? MediaQuery.textScaleFactorOf(context),
textAlign: widget.textAlign,
textDirection: _textDirection,
locale: widget.locale,
textHeightBehavior: widget.textHeightBehavior ?? DefaultTextHeightBehavior.of(context),
textWidthBasis: widget.textWidthBasis,
obscuringCharacter: widget.obscuringCharacter,
obscureText: widget.obscureText,
autocorrect: widget.autocorrect,
smartDashesType: widget.smartDashesType,
smartQuotesType: widget.smartQuotesType,
enableSuggestions: widget.enableSuggestions,
offset: offset,
onCaretChanged: _handleCaretChanged,
rendererIgnoresPointer: widget.rendererIgnoresPointer,
cursorWidth: widget.cursorWidth,
cursorHeight: widget.cursorHeight,
cursorRadius: widget.cursorRadius,
cursorOffset: widget.cursorOffset ?? Offset.zero,
selectionHeightStyle: widget.selectionHeightStyle,
selectionWidthStyle: widget.selectionWidthStyle,
paintCursorAboveText: widget.paintCursorAboveText,
enableInteractiveSelection: widget.enableInteractiveSelection,
textSelectionDelegate: this,
devicePixelRatio: _devicePixelRatio,
promptRectRange: _currentPromptRectRange,
promptRectColor: widget.autocorrectionTextRectColor,
clipBehavior: widget.clipBehavior,
),
),
);
@ -3192,24 +3067,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
}
return TextSpan(style: widget.style, text: text);
}
if (_placeholderLocation >= 0 && _placeholderLocation <= _value.text.length) {
final List<_ScribblePlaceholder> placeholders = <_ScribblePlaceholder>[];
final int placeholderLocation = _value.text.length - _placeholderLocation;
if (_isMultiline) {
// The zero size placeholder here allows the line to break and keep the caret on the first line.
placeholders.add(const _ScribblePlaceholder(child: SizedBox(), size: Size.zero));
placeholders.add(_ScribblePlaceholder(child: const SizedBox(), size: Size(renderEditable.size.width, 0.0)));
} else {
placeholders.add(const _ScribblePlaceholder(child: SizedBox(), size: Size(100.0, 0.0)));
}
return TextSpan(style: widget.style, children: <InlineSpan>[
TextSpan(text: _value.text.substring(0, placeholderLocation)),
...placeholders,
TextSpan(text: _value.text.substring(placeholderLocation)),
],
);
}
// Read only mode should not paint text composing.
return widget.controller.buildTextSpan(
context: context,
@ -3414,142 +3271,6 @@ class _Editable extends MultiChildRenderObjectWidget {
}
}
class _ScribbleFocusable extends StatefulWidget {
const _ScribbleFocusable({
Key? key,
required this.child,
required this.focusNode,
required this.editableKey,
required this.updateSelectionRects,
required this.enabled,
}): super(key: key);
final Widget child;
final FocusNode focusNode;
final GlobalKey editableKey;
final VoidCallback updateSelectionRects;
final bool enabled;
@override
_ScribbleFocusableState createState() => _ScribbleFocusableState();
}
class _ScribbleFocusableState extends State<_ScribbleFocusable> implements ScribbleClient {
_ScribbleFocusableState(): _elementIdentifier = (_nextElementIdentifier++).toString();
@override
void initState() {
super.initState();
if (widget.enabled) {
TextInput.registerScribbleElement(elementIdentifier, this);
}
}
@override
void didUpdateWidget(_ScribbleFocusable oldWidget) {
super.didUpdateWidget(oldWidget);
if (!oldWidget.enabled && widget.enabled) {
TextInput.registerScribbleElement(elementIdentifier, this);
}
if (oldWidget.enabled && !widget.enabled) {
TextInput.unregisterScribbleElement(elementIdentifier);
}
}
@override
void dispose() {
TextInput.unregisterScribbleElement(elementIdentifier);
super.dispose();
}
RenderEditable? get renderEditable => widget.editableKey.currentContext?.findRenderObject() as RenderEditable?;
static int _nextElementIdentifier = 1;
final String _elementIdentifier;
@override
String get elementIdentifier => _elementIdentifier;
@override
void onScribbleFocus(Offset offset) {
widget.focusNode.requestFocus();
renderEditable?.selectPositionAt(from: offset, cause: SelectionChangedCause.scribble);
widget.updateSelectionRects();
}
@override
bool isInScribbleRect(Rect rect) {
final Rect _bounds = bounds;
if (renderEditable?.readOnly ?? false)
return false;
if (_bounds == Rect.zero)
return false;
if (!_bounds.overlaps(rect))
return false;
final Rect intersection = _bounds.intersect(rect);
final HitTestResult result = HitTestResult();
WidgetsBinding.instance?.hitTest(result, intersection.center);
return result.path.any((HitTestEntry entry) => entry.target == renderEditable);
}
@override
Rect get bounds {
final RenderBox? box = context.findRenderObject() as RenderBox?;
if (box == null || !mounted || !box.attached)
return Rect.zero;
final Matrix4 transform = box.getTransformTo(null);
return MatrixUtils.transformRect(transform, Rect.fromLTWH(0, 0, box.size.width, box.size.height));
}
@override
Widget build(BuildContext context) {
return widget.child;
}
}
class _ScribblePlaceholder extends WidgetSpan {
const _ScribblePlaceholder({
required Widget child,
ui.PlaceholderAlignment alignment = ui.PlaceholderAlignment.bottom,
TextBaseline? baseline,
TextStyle? style,
required this.size,
}) : assert(child != null),
assert(baseline != null || !(
identical(alignment, ui.PlaceholderAlignment.aboveBaseline) ||
identical(alignment, ui.PlaceholderAlignment.belowBaseline) ||
identical(alignment, ui.PlaceholderAlignment.baseline)
)),
super(
alignment: alignment,
baseline: baseline,
style: style,
child: child,
);
/// The size of the span, used in place of adding a placeholder size to the [TextPainter].
final Size size;
@override
void build(ui.ParagraphBuilder builder, { double textScaleFactor = 1.0, List<PlaceholderDimensions>? dimensions }) {
assert(debugAssertIsValid());
final bool hasStyle = style != null;
if (hasStyle) {
builder.pushStyle(style!.getTextStyle(textScaleFactor: textScaleFactor));
}
builder.addPlaceholder(
size.width,
size.height,
alignment,
scale: textScaleFactor,
);
if (hasStyle) {
builder.pop();
}
}
}
/// An interface for retriving the logical text boundary (left-closed-right-open)
/// at a given location in a document.
///

View file

@ -2368,7 +2368,7 @@ void main() {
);
final RenderEditable renderEditable = tester.renderObject<RenderEditable>(
find.byElementPredicate((Element element) => element.renderObject is RenderEditable).last,
find.byElementPredicate((Element element) => element.renderObject is RenderEditable),
);
List<TextSelectionPoint> lastCharEndpoint = renderEditable.getEndpointsForSelection(
@ -3166,7 +3166,7 @@ void main() {
expect(
tester.renderObject<RenderEditable>(
find.byElementPredicate((Element element) => element.renderObject is RenderEditable).last,
find.byElementPredicate((Element element) => element.renderObject is RenderEditable),
).text!.style!.color,
isSameColorAs(CupertinoColors.white),
);

View file

@ -9255,38 +9255,6 @@ void main() {
expect(right.opacity.value, equals(1.0));
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('iPad Scribble selection change shows selection handles', (WidgetTester tester) async {
const String testText = 'lorem ipsum';
final TextEditingController controller = TextEditingController(text: testText);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(
controller: controller,
),
),
),
);
await tester.showKeyboard(find.byType(EditableText));
await tester.testTextInput.startScribbleInteraction();
tester.testTextInput.updateEditingValue(const TextEditingValue(
text: testText,
selection: TextSelection(baseOffset: 2, extentOffset: 7),
));
await tester.pumpAndSettle();
final List<FadeTransition> transitions =
find.byType(FadeTransition).evaluate().map((Element e) => e.widget).cast<FadeTransition>().toList();
expect(transitions.length, 2);
final FadeTransition left = transitions[0];
final FadeTransition right = transitions[1];
expect(left.opacity.value, equals(1.0));
expect(right.opacity.value, equals(1.0));
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }));
testWidgets('Tap shows handles but not toolbar', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'abc def ghi',

View file

@ -2,8 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
@ -143,21 +141,6 @@ class FakeAutofillClient implements TextInputClient, AutofillClient {
@override
void autofill(TextEditingValue newEditingValue) => updateEditingValue(newEditingValue);
@override
void showToolbar() {
latestMethodCall = 'showToolbar';
}
@override
void insertTextPlaceholder(Size size) {
latestMethodCall = 'insertTextPlaceholder';
}
@override
void removeTextPlaceholder() {
latestMethodCall = 'removeTextPlaceholder';
}
}
class FakeAutofillScope with AutofillScopeMixin implements AutofillScope {

View file

@ -3,7 +3,6 @@
// found in the LICENSE file.
import 'dart:convert' show jsonDecode;
import 'dart:ui';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
@ -115,20 +114,5 @@ class FakeDeltaTextInputClient implements DeltaTextInputClient {
latestMethodCall = 'showAutocorrectionPromptRect';
}
@override
void insertTextPlaceholder(Size size) {
latestMethodCall = 'insertTextPlaceholder';
}
@override
void removeTextPlaceholder() {
latestMethodCall = 'removeTextPlaceholder';
}
@override
void showToolbar() {
latestMethodCall = 'showToolbar';
}
TextInputConfiguration get configuration => const TextInputConfiguration(enableDeltaModel: true);
}

View file

@ -4,7 +4,6 @@
import 'dart:convert' show jsonDecode;
import 'dart:ui';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
@ -497,148 +496,6 @@ void main() {
expect(client.latestMethodCall, 'showAutocorrectionPromptRect');
});
test('TextInputClient showToolbar method is called', () async {
// Assemble a TextInputConnection so we can verify its change in state.
final FakeTextInputClient client = FakeTextInputClient(TextEditingValue.empty);
const TextInputConfiguration configuration = TextInputConfiguration();
TextInput.attach(client, configuration);
expect(client.latestMethodCall, isEmpty);
// Send showToolbar message.
final ByteData? messageBytes =
const JSONMessageCodec().encodeMessage(<String, dynamic>{
'args': <dynamic>[1, 0, 1],
'method': 'TextInputClient.showToolbar',
});
await ServicesBinding.instance!.defaultBinaryMessenger.handlePlatformMessage(
'flutter/textinput',
messageBytes,
(ByteData? _) {},
);
expect(client.latestMethodCall, 'showToolbar');
});
});
group('Scribble interactions', () {
tearDown(() {
TextInputConnection.debugResetId();
});
test('TextInputClient scribbleInteractionBegan and scribbleInteractionFinished', () async {
// Assemble a TextInputConnection so we can verify its change in state.
final FakeTextInputClient client = FakeTextInputClient(TextEditingValue.empty);
const TextInputConfiguration configuration = TextInputConfiguration();
final TextInputConnection connection = TextInput.attach(client, configuration);
expect(connection.scribbleInProgress, false);
// Send scribbleInteractionBegan message.
ByteData? messageBytes =
const JSONMessageCodec().encodeMessage(<String, dynamic>{
'args': <dynamic>[1, 0, 1],
'method': 'TextInputClient.scribbleInteractionBegan',
});
await ServicesBinding.instance!.defaultBinaryMessenger.handlePlatformMessage(
'flutter/textinput',
messageBytes,
(ByteData? _) {},
);
expect(connection.scribbleInProgress, true);
// Send scribbleInteractionFinished message.
messageBytes =
const JSONMessageCodec().encodeMessage(<String, dynamic>{
'args': <dynamic>[1, 0, 1],
'method': 'TextInputClient.scribbleInteractionFinished',
});
await ServicesBinding.instance!.defaultBinaryMessenger.handlePlatformMessage(
'flutter/textinput',
messageBytes,
(ByteData? _) {},
);
expect(connection.scribbleInProgress, false);
});
test('TextInputClient focusElement', () async {
// Assemble a TextInputConnection so we can verify its change in state.
final FakeTextInputClient client = FakeTextInputClient(TextEditingValue.empty);
const TextInputConfiguration configuration = TextInputConfiguration();
TextInput.attach(client, configuration);
final FakeScribbleElement targetElement = FakeScribbleElement(elementIdentifier: 'target');
TextInput.registerScribbleElement(targetElement.elementIdentifier, targetElement);
final FakeScribbleElement otherElement = FakeScribbleElement(elementIdentifier: 'other');
TextInput.registerScribbleElement(otherElement.elementIdentifier, otherElement);
expect(targetElement.latestMethodCall, isEmpty);
expect(otherElement.latestMethodCall, isEmpty);
// Send focusElement message.
final ByteData? messageBytes =
const JSONMessageCodec().encodeMessage(<String, dynamic>{
'args': <dynamic>[targetElement.elementIdentifier, 0.0, 0.0],
'method': 'TextInputClient.focusElement',
});
await ServicesBinding.instance!.defaultBinaryMessenger.handlePlatformMessage(
'flutter/textinput',
messageBytes,
(ByteData? _) {},
);
TextInput.unregisterScribbleElement(targetElement.elementIdentifier);
TextInput.unregisterScribbleElement(otherElement.elementIdentifier);
expect(targetElement.latestMethodCall, 'onScribbleFocus');
expect(otherElement.latestMethodCall, isEmpty);
});
test('TextInputClient requestElementsInRect', () async {
// Assemble a TextInputConnection so we can verify its change in state.
final FakeTextInputClient client = FakeTextInputClient(TextEditingValue.empty);
const TextInputConfiguration configuration = TextInputConfiguration();
TextInput.attach(client, configuration);
final List<FakeScribbleElement> targetElements = <FakeScribbleElement>[
FakeScribbleElement(elementIdentifier: 'target1', bounds: const Rect.fromLTWH(0.0, 0.0, 100.0, 100.0)),
FakeScribbleElement(elementIdentifier: 'target2', bounds: const Rect.fromLTWH(0.0, 100.0, 100.0, 100.0)),
];
final List<FakeScribbleElement> otherElements = <FakeScribbleElement>[
FakeScribbleElement(elementIdentifier: 'other1', bounds: const Rect.fromLTWH(100.0, 0.0, 100.0, 100.0)),
FakeScribbleElement(elementIdentifier: 'other2', bounds: const Rect.fromLTWH(100.0, 100.0, 100.0, 100.0)),
];
void registerElements(FakeScribbleElement element) => TextInput.registerScribbleElement(element.elementIdentifier, element);
void unregisterElements(FakeScribbleElement element) => TextInput.unregisterScribbleElement(element.elementIdentifier);
<FakeScribbleElement>[...targetElements, ...otherElements].forEach(registerElements);
// Send requestElementsInRect message.
final ByteData? messageBytes =
const JSONMessageCodec().encodeMessage(<String, dynamic>{
'args': <dynamic>[0.0, 50.0, 50.0, 100.0],
'method': 'TextInputClient.requestElementsInRect',
});
ByteData? responseBytes;
await ServicesBinding.instance!.defaultBinaryMessenger.handlePlatformMessage(
'flutter/textinput',
messageBytes,
(ByteData? response) {
responseBytes = response;
},
);
<FakeScribbleElement>[...targetElements, ...otherElements].forEach(unregisterElements);
final List<List<dynamic>> responses = (const JSONMessageCodec().decodeMessage(responseBytes) as List<dynamic>).cast<List<dynamic>>();
expect(responses.first.length, 2);
expect(responses.first.first, containsAllInOrder(<dynamic>[targetElements.first.elementIdentifier, 0.0, 0.0, 100.0, 100.0]));
expect(responses.first.last, containsAllInOrder(<dynamic>[targetElements.last.elementIdentifier, 0.0, 100.0, 100.0, 100.0]));
});
});
test('TextEditingValue.isComposingRangeValid', () async {
@ -710,20 +567,5 @@ class FakeTextInputClient implements TextInputClient {
latestMethodCall = 'showAutocorrectionPromptRect';
}
@override
void showToolbar() {
latestMethodCall = 'showToolbar';
}
TextInputConfiguration get configuration => const TextInputConfiguration();
@override
void insertTextPlaceholder(Size size) {
latestMethodCall = 'insertTextPlaceholder';
}
@override
void removeTextPlaceholder() {
latestMethodCall = 'removeTextPlaceholder';
}
}

View file

@ -3,7 +3,6 @@
// found in the LICENSE file.
import 'dart:convert' show utf8;
import 'dart:ui';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
@ -65,29 +64,3 @@ class FakeTextChannel implements MethodChannel {
}
}
}
class FakeScribbleElement implements ScribbleClient {
FakeScribbleElement({required String elementIdentifier, Rect bounds = Rect.zero})
: _elementIdentifier = elementIdentifier,
_bounds = bounds;
final String _elementIdentifier;
final Rect _bounds;
String latestMethodCall = '';
@override
Rect get bounds => _bounds;
@override
String get elementIdentifier => _elementIdentifier;
@override
bool isInScribbleRect(Rect rect) {
return _bounds.overlaps(rect);
}
@override
void onScribbleFocus(Offset offset) {
latestMethodCall = 'onScribbleFocus';
}
}

View file

@ -1662,329 +1662,6 @@ void main() {
}
});
testWidgets('Selection changes during Scribble interaction should have the scribble cause', (WidgetTester tester) async {
late SelectionChangedCause selectionCause;
final TextEditingController controller =
TextEditingController(text: 'Lorem ipsum dolor sit amet');
await tester.pumpWidget(
MaterialApp(
home: EditableText(
controller: controller,
backgroundCursorColor: Colors.grey,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
selectionControls: materialTextSelectionControls,
onSelectionChanged: (TextSelection selection, SelectionChangedCause? cause) {
if (cause != null)
selectionCause = cause;
},
),
),
);
await tester.showKeyboard(find.byType(EditableText));
// A normal selection update from the framework has 'keyboard' as the cause.
tester.testTextInput.updateEditingValue(TextEditingValue(
text: controller.text,
selection: const TextSelection(baseOffset: 2, extentOffset: 3),
));
await tester.pumpAndSettle();
expect(selectionCause, SelectionChangedCause.keyboard);
// A selection update during a scribble interaction has 'scribble' as the cause.
await tester.testTextInput.startScribbleInteraction();
tester.testTextInput.updateEditingValue(TextEditingValue(
text: controller.text,
selection: const TextSelection(baseOffset: 3, extentOffset: 4),
));
await tester.pumpAndSettle();
expect(selectionCause, SelectionChangedCause.scribble);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }));
testWidgets('Requests focus and changes the selection when onScribbleFocus is called', (WidgetTester tester) async {
final TextEditingController controller =
TextEditingController(text: 'Lorem ipsum dolor sit amet');
late SelectionChangedCause selectionCause;
await tester.pumpWidget(
MaterialApp(
home: EditableText(
controller: controller,
backgroundCursorColor: Colors.grey,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
selectionControls: materialTextSelectionControls,
onSelectionChanged: (TextSelection selection, SelectionChangedCause? cause) {
if (cause != null)
selectionCause = cause;
},
),
),
);
await tester.testTextInput.scribbleFocusElement(TextInput.scribbleClients.keys.first, Offset.zero);
expect(focusNode.hasFocus, true);
expect(selectionCause, SelectionChangedCause.scribble);
// On web, we should rely on the browser's implementation of Scribble, so the selection changed cause
// will never be SelectionChangedCause.scribble.
}, skip: kIsWeb, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS })); // [intended]
testWidgets('Declares itself for Scribble interaction if the bounds overlap the scribble rect and the widget is touchable', (WidgetTester tester) async {
final TextEditingController controller =
TextEditingController(text: 'Lorem ipsum dolor sit amet');
await tester.pumpWidget(
MaterialApp(
home: EditableText(
controller: controller,
backgroundCursorColor: Colors.grey,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
selectionControls: materialTextSelectionControls,
),
),
);
final List<dynamic> elementEntry = <dynamic>[TextInput.scribbleClients.keys.first, 0.0, 0.0, 800.0, 600.0];
List<List<dynamic>> elements = await tester.testTextInput.scribbleRequestElementsInRect(const Rect.fromLTWH(0, 0, 1, 1));
expect(elements.first, containsAll(elementEntry));
// Touch is outside the bounds of the widget.
elements = await tester.testTextInput.scribbleRequestElementsInRect(const Rect.fromLTWH(-1, -1, 1, 1));
expect(elements.length, 0);
// Widget is read only.
await tester.pumpWidget(
MaterialApp(
home: EditableText(
readOnly: true,
controller: controller,
backgroundCursorColor: Colors.grey,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
selectionControls: materialTextSelectionControls,
),
),
);
elements = await tester.testTextInput.scribbleRequestElementsInRect(const Rect.fromLTWH(0, 0, 1, 1));
expect(elements.length, 0);
// Widget is not touchable.
await tester.pumpWidget(
MaterialApp(
home: Stack(children: <Widget>[
EditableText(
controller: controller,
backgroundCursorColor: Colors.grey,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
selectionControls: materialTextSelectionControls,
),
Positioned(
left: 0,
top: 0,
right: 0,
bottom: 0,
child: Container(color: Colors.black),
),
],
),
),
);
elements = await tester.testTextInput.scribbleRequestElementsInRect(const Rect.fromLTWH(0, 0, 1, 1));
expect(elements.length, 0);
// Widget has scribble disabled.
await tester.pumpWidget(
MaterialApp(
home: EditableText(
controller: controller,
backgroundCursorColor: Colors.grey,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
selectionControls: materialTextSelectionControls,
scribbleEnabled: false,
),
),
);
elements = await tester.testTextInput.scribbleRequestElementsInRect(const Rect.fromLTWH(0, 0, 1, 1));
expect(elements.length, 0);
// On web, we should rely on the browser's implementation of Scribble, so the engine will
// never request the scribble elements.
}, skip: kIsWeb, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS })); // [intended]
testWidgets('single line Scribble fields can show a horizontal placeholder', (WidgetTester tester) async {
final TextEditingController controller =
TextEditingController(text: 'Lorem ipsum dolor sit amet');
await tester.pumpWidget(
MaterialApp(
home: EditableText(
controller: controller,
backgroundCursorColor: Colors.grey,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
selectionControls: materialTextSelectionControls,
),
),
);
await tester.showKeyboard(find.byType(EditableText));
tester.testTextInput.updateEditingValue(TextEditingValue(
text: controller.text,
selection: const TextSelection(baseOffset: 5, extentOffset: 5),
));
await tester.pumpAndSettle();
await tester.testTextInput.scribbleInsertPlaceholder();
await tester.pumpAndSettle();
TextSpan textSpan = findRenderEditable(tester).text! as TextSpan;
expect(textSpan.children!.length, 3);
expect((textSpan.children![0] as TextSpan).text, 'Lorem');
expect(textSpan.children![1] is WidgetSpan, true);
expect((textSpan.children![2] as TextSpan).text, ' ipsum dolor sit amet');
await tester.testTextInput.scribbleRemovePlaceholder();
await tester.pumpAndSettle();
textSpan = findRenderEditable(tester).text! as TextSpan;
expect(textSpan.children, null);
expect(textSpan.text, 'Lorem ipsum dolor sit amet');
// Widget has scribble disabled.
await tester.pumpWidget(
MaterialApp(
home: EditableText(
controller: controller,
backgroundCursorColor: Colors.grey,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
selectionControls: materialTextSelectionControls,
scribbleEnabled: false,
),
),
);
await tester.showKeyboard(find.byType(EditableText));
tester.testTextInput.updateEditingValue(TextEditingValue(
text: controller.text,
selection: const TextSelection(baseOffset: 5, extentOffset: 5),
));
await tester.pumpAndSettle();
await tester.testTextInput.scribbleInsertPlaceholder();
await tester.pumpAndSettle();
textSpan = findRenderEditable(tester).text! as TextSpan;
expect(textSpan.children, null);
expect(textSpan.text, 'Lorem ipsum dolor sit amet');
// On web, we should rely on the browser's implementation of Scribble, so the framework
// will not handle placeholders.
}, skip: kIsWeb, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS })); // [intended]
testWidgets('multiline Scribble fields can show a vertical placeholder', (WidgetTester tester) async {
final TextEditingController controller =
TextEditingController(text: 'Lorem ipsum dolor sit amet');
await tester.pumpWidget(
MaterialApp(
home: EditableText(
controller: controller,
backgroundCursorColor: Colors.grey,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
selectionControls: materialTextSelectionControls,
maxLines: 2,
),
),
);
await tester.showKeyboard(find.byType(EditableText));
tester.testTextInput.updateEditingValue(TextEditingValue(
text: controller.text,
selection: const TextSelection(baseOffset: 5, extentOffset: 5),
));
await tester.pumpAndSettle();
await tester.testTextInput.scribbleInsertPlaceholder();
await tester.pumpAndSettle();
TextSpan textSpan = findRenderEditable(tester).text! as TextSpan;
expect(textSpan.children!.length, 4);
expect((textSpan.children![0] as TextSpan).text, 'Lorem');
expect(textSpan.children![1] is WidgetSpan, true);
expect(textSpan.children![2] is WidgetSpan, true);
expect((textSpan.children![3] as TextSpan).text, ' ipsum dolor sit amet');
await tester.testTextInput.scribbleRemovePlaceholder();
await tester.pumpAndSettle();
textSpan = findRenderEditable(tester).text! as TextSpan;
expect(textSpan.children, null);
expect(textSpan.text, 'Lorem ipsum dolor sit amet');
// Widget has scribble disabled.
await tester.pumpWidget(
MaterialApp(
home: EditableText(
controller: controller,
backgroundCursorColor: Colors.grey,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
selectionControls: materialTextSelectionControls,
maxLines: 2,
scribbleEnabled: false,
),
),
);
await tester.showKeyboard(find.byType(EditableText));
tester.testTextInput.updateEditingValue(TextEditingValue(
text: controller.text,
selection: const TextSelection(baseOffset: 5, extentOffset: 5),
));
await tester.pumpAndSettle();
await tester.testTextInput.scribbleInsertPlaceholder();
await tester.pumpAndSettle();
textSpan = findRenderEditable(tester).text! as TextSpan;
expect(textSpan.children, null);
expect(textSpan.text, 'Lorem ipsum dolor sit amet');
// On web, we should rely on the browser's implementation of Scribble, so the framework
// will not handle placeholders.
}, skip: kIsWeb, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS })); // [intended]
testWidgets('Sends "updateConfig" when read-only flag is flipped', (WidgetTester tester) async {
bool readOnly = true;
late StateSetter setState;
@ -4017,85 +3694,6 @@ void main() {
);
});
testWidgets('selection rects are sent when they change', (WidgetTester tester) async {
final List<MethodCall> log = <MethodCall>[];
SystemChannels.textInput.setMockMethodCallHandler((MethodCall methodCall) async {
log.add(methodCall);
});
final TextEditingController controller = TextEditingController();
controller.text = 'Text1';
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(),
child: Directionality(
textDirection: TextDirection.ltr,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
EditableText(
key: ValueKey<String>(controller.text),
controller: controller,
focusNode: FocusNode(),
style: Typography.material2018().black.subtitle1!,
cursorColor: Colors.blue,
backgroundCursorColor: Colors.grey,
),
],
),
),
),
);
await tester.showKeyboard(find.byKey(ValueKey<String>(controller.text)));
// There should be a new platform message updating the selection rects.
final MethodCall methodCall = log.firstWhere((MethodCall m) => m.method == 'TextInput.setSelectionRects');
expect(methodCall.method, 'TextInput.setSelectionRects');
expect((methodCall.arguments as List<dynamic>).length, 5);
// On web, we should rely on the browser's implementation of Scribble, so we will not send selection rects.
}, skip: kIsWeb, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS })); // [intended]
testWidgets('selection rects are not sent if scribbleEnabled is false', (WidgetTester tester) async {
final List<MethodCall> log = <MethodCall>[];
SystemChannels.textInput.setMockMethodCallHandler((MethodCall methodCall) async {
log.add(methodCall);
});
final TextEditingController controller = TextEditingController();
controller.text = 'Text1';
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(),
child: Directionality(
textDirection: TextDirection.ltr,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
EditableText(
key: ValueKey<String>(controller.text),
controller: controller,
focusNode: FocusNode(),
style: Typography.material2018().black.subtitle1!,
cursorColor: Colors.blue,
backgroundCursorColor: Colors.grey,
scribbleEnabled: false,
),
],
),
),
),
);
await tester.showKeyboard(find.byKey(ValueKey<String>(controller.text)));
// There should be a new platform message updating the selection rects.
expect(log.where((MethodCall m) => m.method == 'TextInput.setSelectionRects').length, 0);
// On web, we should rely on the browser's implementation of Scribble, so we will not send selection rects.
}, skip: kIsWeb, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS })); // [intended]
testWidgets('text styling info is sent on show keyboard', (WidgetTester tester) async {
final List<MethodCall> log = <MethodCall>[];
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.textInput, (MethodCall methodCall) async {

View file

@ -3,8 +3,6 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:typed_data';
import 'dart:ui' show Rect, Offset;
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
@ -273,84 +271,4 @@ class TestTextInput {
(ByteData? data) { /* response from framework is discarded */ },
);
}
/// Simulates a scribble interaction starting.
Future<void> startScribbleInteraction() async {
assert(isRegistered);
await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.handlePlatformMessage(
SystemChannels.textInput.name,
SystemChannels.textInput.codec.encodeMethodCall(
MethodCall(
'TextInputClient.scribbleInteractionBegan',
<dynamic>[_client ?? -1,]
),
),
(ByteData? data) { /* response from framework is discarded */ },
);
}
/// Simulates a Scribble focus.
Future<void> scribbleFocusElement(String elementIdentifier, Offset offset) async {
assert(isRegistered);
await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.handlePlatformMessage(
SystemChannels.textInput.name,
SystemChannels.textInput.codec.encodeMethodCall(
MethodCall(
'TextInputClient.focusElement',
<dynamic>[elementIdentifier, offset.dx, offset.dy]
),
),
(ByteData? data) { /* response from framework is discarded */ },
);
}
/// Simulates iOS asking for the list of Scribble elements during UIIndirectScribbleInteraction.
Future<List<List<dynamic>>> scribbleRequestElementsInRect(Rect rect) async {
assert(isRegistered);
List<List<dynamic>> response = <List<dynamic>>[];
await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.handlePlatformMessage(
SystemChannels.textInput.name,
SystemChannels.textInput.codec.encodeMethodCall(
MethodCall(
'TextInputClient.requestElementsInRect',
<dynamic>[rect.left, rect.top, rect.width, rect.height]
),
),
(ByteData? data) {
response = (SystemChannels.textInput.codec.decodeEnvelope(data!) as List<dynamic>).map((dynamic element) => element as List<dynamic>).toList();
},
);
return response;
}
/// Simulates iOS inserting a UITextPlaceholder during a long press with the pencil.
Future<void> scribbleInsertPlaceholder() async {
assert(isRegistered);
await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.handlePlatformMessage(
SystemChannels.textInput.name,
SystemChannels.textInput.codec.encodeMethodCall(
MethodCall(
'TextInputClient.insertTextPlaceholder',
<dynamic>[_client ?? -1, 0.0, 0.0]
),
),
(ByteData? data) { /* response from framework is discarded */ },
);
}
/// Simulates iOS removing a UITextPlaceholder after a long press with the pencil is released.
Future<void> scribbleRemovePlaceholder() async {
assert(isRegistered);
await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.handlePlatformMessage(
SystemChannels.textInput.name,
SystemChannels.textInput.codec.encodeMethodCall(
MethodCall(
'TextInputClient.removeTextPlaceholder',
<dynamic>[_client ?? -1]
),
),
(ByteData? data) { /* response from framework is discarded */ },
);
}
}