From 009fa69f3c4e2471eb45c5760780a4e40d319f66 Mon Sep 17 00:00:00 2001 From: Casey Hillers Date: Thu, 3 Nov 2022 17:17:30 -0700 Subject: [PATCH] Revert "Scribble mixin (#104128)" (#114647) This reverts commit b571abfbfd17b9ab7bdf73ac9758bbd87264979a. --- packages/flutter/lib/services.dart | 1 - .../flutter/lib/src/services/binding.dart | 5 +- .../flutter/lib/src/services/scribble.dart | 243 ------------------ .../lib/src/services/system_channels.dart | 32 --- .../flutter/lib/src/services/text_input.dart | 175 ++++++++++++- .../lib/src/widgets/editable_text.dart | 184 +++++-------- .../flutter/test/services/autofill_test.dart | 15 ++ .../flutter/test/services/binding_test.dart | 7 + .../test/services/delta_text_input_test.dart | 15 ++ .../flutter/test/services/scribble_test.dart | 213 --------------- .../test/services/text_input_test.dart | 174 ++++++++++++- .../test/services/text_input_utils.dart | 17 +- .../test/widgets/editable_text_test.dart | 79 +----- .../flutter_test/lib/src/test_text_input.dart | 32 +-- 14 files changed, 462 insertions(+), 730 deletions(-) delete mode 100644 packages/flutter/lib/src/services/scribble.dart delete mode 100644 packages/flutter/test/services/scribble_test.dart diff --git a/packages/flutter/lib/services.dart b/packages/flutter/lib/services.dart index 74d18f333c4..a0ff42f6772 100644 --- a/packages/flutter/lib/services.dart +++ b/packages/flutter/lib/services.dart @@ -37,7 +37,6 @@ export 'src/services/raw_keyboard_macos.dart'; export 'src/services/raw_keyboard_web.dart'; export 'src/services/raw_keyboard_windows.dart'; export 'src/services/restoration.dart'; -export 'src/services/scribble.dart'; export 'src/services/service_extensions.dart'; export 'src/services/spell_check.dart'; export 'src/services/system_channels.dart'; diff --git a/packages/flutter/lib/src/services/binding.dart b/packages/flutter/lib/src/services/binding.dart index 3f92afa0c30..b9531f419c2 100644 --- a/packages/flutter/lib/src/services/binding.dart +++ b/packages/flutter/lib/src/services/binding.dart @@ -15,9 +15,9 @@ import 'binary_messenger.dart'; import 'hardware_keyboard.dart'; import 'message_codec.dart'; import 'restoration.dart'; -import 'scribble.dart'; import 'service_extensions.dart'; import 'system_channels.dart'; +import 'text_input.dart'; export 'dart:ui' show ChannelBuffers, RootIsolateToken; @@ -43,7 +43,7 @@ mixin ServicesBinding on BindingBase, SchedulerBinding { SystemChannels.system.setMessageHandler((dynamic message) => handleSystemMessage(message as Object)); SystemChannels.lifecycle.setMessageHandler(_handleLifecycleMessage); SystemChannels.platform.setMethodCallHandler(_handlePlatformMessage); - Scribble.ensureInitialized(); + TextInput.ensureInitialized(); readInitialLifecycleStateFromNativeWindow(); } @@ -326,6 +326,7 @@ mixin ServicesBinding on BindingBase, SchedulerBinding { void setSystemUiChangeCallback(SystemUiChangeCallback? callback) { _systemUiChangeCallback = callback; } + } /// Signature for listening to changes in the [SystemUiMode]. diff --git a/packages/flutter/lib/src/services/scribble.dart b/packages/flutter/lib/src/services/scribble.dart deleted file mode 100644 index 3548868cb7c..00000000000 --- a/packages/flutter/lib/src/services/scribble.dart +++ /dev/null @@ -1,243 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// 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/foundation.dart'; - -import 'message_codec.dart'; -import 'platform_channel.dart'; -import 'system_channels.dart'; - -/// An interface into system-level handwriting text input. -/// -/// This is typically used by implemeting the methods in [ScribbleClient] in a -/// class, usually a [State], and setting an instance of it to [client]. The -/// relevant methods on [ScribbleClient] will be called in response to method -/// channel calls on [SystemChannels.scribble]. -/// -/// Currently, handwriting input is supported in the iOS embedder with the Apple -/// Pencil. -/// -/// [EditableText] uses this class via [ScribbleClient] to automatically support -/// handwriting input when [EditableText.scribbleEnabled] is set to true. -/// -/// See also: -/// -/// * [SystemChannels.scribble], which is the [MethodChannel] used by this -/// class, and which has a list of the methods that this class handles. -class Scribble { - Scribble._() { - _channel.setMethodCallHandler(_handleScribbleInvocation); - } - - /// Ensure that a [Scribble] instance has been set up so that the platform - /// can handle messages on the scribble method channel. - static void ensureInitialized() { - _instance; // ignore: unnecessary_statements - } - - /// Set the [MethodChannel] used to communicate with the system's text input - /// control. - /// - /// This is only meant for testing within the Flutter SDK. Changing this - /// will break the ability to do handwriting input. This has no effect if - /// asserts are disabled. - @visibleForTesting - static void setChannel(MethodChannel newChannel) { - assert(() { - _instance._channel = newChannel..setMethodCallHandler(_instance._handleScribbleInvocation); - return true; - }()); - } - - static final Scribble _instance = Scribble._(); - - /// Set the given [ScribbleClient] as the single active client. - /// - /// This is usually based on the [ScribbleClient] receiving focus. - static set client(ScribbleClient? client) { - _instance._client = client; - } - - /// Return the current active [ScribbleClient], or null if none. - static ScribbleClient? get client => _instance._client; - - ScribbleClient? _client; - - MethodChannel _channel = SystemChannels.scribble; - - final Map _scribbleClients = {}; - bool _scribbleInProgress = false; - - /// Used for testing within the Flutter SDK to get the currently registered [ScribbleClient] list. - @visibleForTesting - static Map get scribbleClients => Scribble._instance._scribbleClients; - - /// Returns true if a scribble interaction is currently happening. - static bool get scribbleInProgress => _instance._scribbleInProgress; - - Future _handleScribbleInvocation(MethodCall methodCall) async { - final String method = methodCall.method; - if (method == 'Scribble.focusElement') { - final List args = methodCall.arguments as List; - _scribbleClients[args[0]]?.onScribbleFocus(Offset((args[1] as num).toDouble(), (args[2] as num).toDouble())); - return; - } else if (method == 'Scribble.requestElementsInRect') { - final List args = (methodCall.arguments as List).cast().map((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 [elementIdentifier, ...[bounds.left, bounds.top, bounds.width, bounds.height]]; - }).toList(); - } else if (method == 'Scribble.scribbleInteractionBegan') { - _scribbleInProgress = true; - return; - } else if (method == 'Scribble.scribbleInteractionFinished') { - _scribbleInProgress = false; - return; - } - - // The methods below are only valid when a client exists, i.e. when a field - // is focused. - final ScribbleClient? client = _client; - if (client == null) { - return; - } - - final List args = methodCall.arguments as List; - switch (method) { - case 'Scribble.showToolbar': - client.showToolbar(); - break; - case 'Scribble.insertTextPlaceholder': - client.insertTextPlaceholder(Size((args[1] as num).toDouble(), (args[2] as num).toDouble())); - break; - case 'Scribble.removeTextPlaceholder': - client.removeTextPlaceholder(); - break; - default: - throw MissingPluginException(); - } - } - - /// 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) { - _instance._scribbleClients[elementIdentifier] = scribbleClient; - } - - /// Unregisters a [ScribbleClient] with [elementIdentifier]. - static void unregisterScribbleElement(String elementIdentifier) { - _instance._scribbleClients.remove(elementIdentifier); - } - - List _cachedSelectionRects = []; - - /// 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. - static void setSelectionRects(List selectionRects) { - if (!listEquals(_instance._cachedSelectionRects, selectionRects)) { - _instance._cachedSelectionRects = selectionRects; - _instance._channel.invokeMethod( - 'Scribble.setSelectionRects', - selectionRects.map((SelectionRect rect) { - return [rect.bounds.left, rect.bounds.top, rect.bounds.width, rect.bounds.height, rect.position]; - }).toList(), - ); - } - } -} - -/// An interface to interact with the engine for handwriting text input. -/// -/// This is currently only used to handle -/// [UIIndirectScribbleInteraction](https://developer.apple.com/documentation/uikit/uiindirectscribbleinteraction), -/// which is responsible for manually receiving handwritten text input in UIKit. -/// The Flutter engine uses this to receive handwriting input on Flutter text -/// input fields. -mixin 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. - /// - /// The [Offset] indicates the location where the focus event happened, which - /// is typically where the cursor should be placed. - void onScribbleFocus(Offset offset); - - /// Tests whether the [ScribbleClient] overlaps the given rectangle bounds, - /// where the rectangle bounds are in global coordinates. - bool isInScribbleRect(Rect rect); - - /// The current bounds of the [ScribbleClient]. - Rect get bounds; - - /// Requests that the client show the editing toolbar. - /// - /// This is used when the platform changes the selection during scribble - /// input. - 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(); -} - -/// 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 => Object.hash(position, bounds); - - @override - String toString() => 'SelectionRect($position, $bounds)'; -} diff --git a/packages/flutter/lib/src/services/system_channels.dart b/packages/flutter/lib/src/services/system_channels.dart index 7c7889a6f98..d8b661166fe 100644 --- a/packages/flutter/lib/src/services/system_channels.dart +++ b/packages/flutter/lib/src/services/system_channels.dart @@ -222,38 +222,6 @@ class SystemChannels { JSONMethodCodec(), ); - /// A JSON [MethodChannel] for handling handwriting input. - /// - /// This method channel is used by iPadOS 14's Scribble feature where writing - /// with an Apple Pencil on top of a text field inserts text into the field. - /// - /// The following methods are defined for this channel: - /// - /// * `Scribble.focusElement`: Indicates that focus is requested at the given - /// [Offset]. - /// - /// * `Scribble.requestElementsInRect`: Returns a List of identifiers and - /// bounds for the [ScribbleClient]s that lie within the given Rect. - /// - /// * `Scribble.scribbleInteractionBegan`: Indicates that handwriting input - /// has started. - /// - /// * `Scribble.scribbleInteractionFinished`: Indicates that handwriting input - /// has ended. - /// - /// * `Scribble.showToolbar`: Requests that the toolbar be shown, such as - /// when selection is changed by handwriting. - /// - /// * `Scribble.insertTextPlaceholder`: Requests that visual writing space is - /// reserved. - /// - /// * `Scribble.removeTextPlaceholder`: Requests that any placeholder writing - /// space is removed. - static const MethodChannel scribble = OptionalMethodChannel( - 'flutter/scribble', - JSONMethodCodec(), - ); - /// A [MethodChannel] for handling spell check for text input. /// /// This channel exposes the spell check framework for supported platforms. diff --git a/packages/flutter/lib/src/services/text_input.dart b/packages/flutter/lib/src/services/text_input.dart index 1016d05a9b7..b2ccef4320b 100644 --- a/packages/flutter/lib/src/services/text_input.dart +++ b/packages/flutter/lib/src/services/text_input.dart @@ -1162,12 +1162,84 @@ mixin TextInputClient { /// * [TextInputControl.show], a method to show the new input control. void didChangeInputControl(TextInputControl? oldControl, TextInputControl? newControl) {} + /// 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() {} + /// Performs the specified MacOS-specific selector from the /// `NSStandardKeyBindingResponding` protocol or user-specified selector /// from `DefaultKeyBinding.Dict`. void performSelector(String selectorName) {} } +/// 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 => Object.hash(position, bounds); + + @override + String toString() => 'SelectionRect($position, $bounds)'; +} + /// An interface to receive granular information from [TextInput]. /// /// See also: @@ -1227,6 +1299,7 @@ class TextInputConnection { Matrix4? _cachedTransform; Rect? _cachedRect; Rect? _cachedCaretRect; + List _cachedSelectionRects = []; static int _nextId = 1; final int _id; @@ -1249,6 +1322,12 @@ 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); @@ -1329,6 +1408,17 @@ class TextInputConnection { TextInput._instance._setCaretRect(validRect); } + /// 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 selectionRects) { + if (!listEquals(_cachedSelectionRects, selectionRects)) { + _cachedSelectionRects = selectionRects; + TextInput._instance._setSelectionRects(selectionRects); + } + } + /// Send text styling information. /// /// This information is used by the Flutter Web Engine to change the style @@ -1586,10 +1676,6 @@ class TextInput { /// Ensure that a [TextInput] instance has been set up so that the platform /// can handle messages on the text input method channel. - @Deprecated( - 'Use Scribble.ensureInitialized instead. ' - 'This feature was deprecated after v3.1.0-9.0.pre.' - ) static void ensureInitialized() { _instance; // ignore: unnecessary_statements } @@ -1652,6 +1738,16 @@ class TextInput { TextInputConnection? _currentConnection; late TextInputConfiguration _currentConfiguration; + final Map _scribbleClients = {}; + bool _scribbleInProgress = false; + + /// Used for testing within the Flutter SDK to get the currently registered [ScribbleClient] list. + @visibleForTesting + static Map get scribbleClients => TextInput._instance._scribbleClients; + + /// Returns true if a scribble interaction is currently happening. + bool get scribbleInProgress => _scribbleInProgress; + Future _loudlyHandleTextInputInvocation(MethodCall call) async { try { return await _handleTextInputInvocation(call); @@ -1668,8 +1764,33 @@ class TextInput { rethrow; } } + Future _handleTextInputInvocation(MethodCall methodCall) async { final String method = methodCall.method; + if (method == 'TextInputClient.focusElement') { + final List args = methodCall.arguments as List; + _scribbleClients[args[0]]?.onScribbleFocus(Offset((args[1] as num).toDouble(), (args[2] as num).toDouble())); + return; + } else if (method == 'TextInputClient.requestElementsInRect') { + final List args = (methodCall.arguments as List).cast().map((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 [elementIdentifier, ...[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; } @@ -1773,6 +1894,15 @@ 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(); } @@ -1856,6 +1986,12 @@ class TextInput { } } + void _setSelectionRects(List selectionRects) { + for (final TextInputControl control in _inputControls) { + control.setSelectionRects(selectionRects); + } + } + void _setStyle({ required String? fontFamily, required double? fontSize, @@ -1955,6 +2091,20 @@ class TextInput { control.finishAutofillContext(shouldSave: 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); + } } /// An interface for implementing text input controls that receive text editing @@ -2038,6 +2188,12 @@ mixin TextInputControl { /// changes. void setCaretRect(Rect rect) {} + /// Informs the text input control about selection area changes. + /// + /// This method is called when the attached input client's selection area + /// changes. + void setSelectionRects(List selectionRects) {} + /// Informs the text input control about text style changes. /// /// This method is called on the when the attached input client's text style @@ -2160,6 +2316,17 @@ class _PlatformTextInputControl with TextInputControl { ); } + @override + void setSelectionRects(List selectionRects) { + _channel.invokeMethod( + 'TextInput.setSelectionRects', + selectionRects.map((SelectionRect rect) { + return [rect.bounds.left, rect.bounds.top, rect.bounds.width, rect.bounds.height, rect.position]; + }).toList(), + ); + } + + @override void setStyle({ required String? fontFamily, diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index b22b27ff253..41a6cf7c435 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -2558,12 +2558,7 @@ class EditableTextState extends State with AutomaticKeepAliveClien if (value.text == _value.text && value.composing == _value.composing) { // `selection` is the only change. - _handleSelectionChanged( - value.selection, - Scribble.scribbleInProgress - ? SelectionChangedCause.scribble - : SelectionChangedCause.keyboard, - ); + _handleSelectionChanged(value.selection, (_textInputConnection?.scribbleInProgress ?? false) ? SelectionChangedCause.scribble : SelectionChangedCause.keyboard); } else { // Only hide the toolbar overlay, the selection handle's visibility will be handled // by `_handleSelectionChanged`. https://github.com/flutter/flutter/issues/108673 @@ -3522,7 +3517,7 @@ class EditableTextState extends State with AutomaticKeepAliveClien } graphemeStart = graphemeEnd; } - Scribble.setSelectionRects(rects); + _textInputConnection!.setSelectionRects(rects); } void _updateSizeAndTransform() { @@ -3533,7 +3528,7 @@ class EditableTextState extends State with AutomaticKeepAliveClien _updateSelectionRects(); SchedulerBinding.instance.addPostFrameCallback((Duration _) => _updateSizeAndTransform()); } else if (_placeholderLocation != -1) { - _removeTextPlaceholder(); + removeTextPlaceholder(); } } @@ -3626,6 +3621,7 @@ class EditableTextState extends State 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 @@ -3696,6 +3692,39 @@ class EditableTextState extends State 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 void performSelector(String selectorName) { final Intent? intent = intentForMacOSSelector(selectorName); @@ -3978,35 +4007,6 @@ class EditableTextState extends State with AutomaticKeepAliveClien return Actions.invoke(context, intent); } - // 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; - - void _onPlaceholderLocationChanged(int location) { - setState(() { - _placeholderLocation = location; - }); - } - - void _onScribbleFocus(Offset offset) { - widget.focusNode.requestFocus(); - renderEditable.selectPositionAt(from: offset, cause: SelectionChangedCause.scribble); - _openInputConnection(); - _updateSelectionRects(force: true); - } - - void _removeTextPlaceholder() { - if (!widget.scribbleEnabled) { - return; - } - - setState(() { - _placeholderLocation = -1; - }); - } /// The default behavior used if [onTapOutside] is null. /// @@ -4121,12 +4121,12 @@ class EditableTextState extends State with AutomaticKeepAliveClien onPaste: _semanticsOnPaste(controls), child: _ScribbleFocusable( focusNode: widget.focusNode, + editableKey: _editableKey, enabled: widget.scribbleEnabled, - onPlaceholderLocationChanged: _onPlaceholderLocationChanged, - onScribbleFocus: _onScribbleFocus, - onShowToolbar: showToolbar, - readOnly: widget.readOnly, - value: _value, + updateSelectionRects: () { + _openInputConnection(); + _updateSelectionRects(force: true); + }, child: _Editable( key: _editableKey, startHandleLayerLink: _startHandleLayerLink, @@ -4441,13 +4441,6 @@ class _Editable extends MultiChildRenderObjectWidget { } } -/// A function that that takes a placeholder location as an int offset into some -/// text. -typedef _PlaceholderLocationCallback = void Function(int location); - -/// A function that takes the Offset at which focus is requested. -typedef _ScribbleFocusCallback = void Function(Offset offset); - @immutable class _ScribbleCacheKey { const _ScribbleCacheKey({ @@ -4488,88 +4481,55 @@ class _ScribbleCacheKey { } } -/// A widget that provides the ability to receive handwriting input from -/// [Scribble]. class _ScribbleFocusable extends StatefulWidget { const _ScribbleFocusable({ required this.child, - required this.enabled, required this.focusNode, - required this.onPlaceholderLocationChanged, - required this.onScribbleFocus, - required this.onShowToolbar, - required this.readOnly, - required this.value, + required this.editableKey, + required this.updateSelectionRects, + required this.enabled, }); final Widget child; - final bool enabled; final FocusNode focusNode; - final _PlaceholderLocationCallback onPlaceholderLocationChanged; - final _ScribbleFocusCallback onScribbleFocus; - final VoidCallback onShowToolbar; - final bool readOnly; - final TextEditingValue value; + final GlobalKey editableKey; + final VoidCallback updateSelectionRects; + final bool enabled; @override _ScribbleFocusableState createState() => _ScribbleFocusableState(); } -class _ScribbleFocusableState extends State<_ScribbleFocusable> with ScribbleClient { +class _ScribbleFocusableState extends State<_ScribbleFocusable> implements ScribbleClient { _ScribbleFocusableState(): _elementIdentifier = (_nextElementIdentifier++).toString(); - void _onFocusChange() { - _updateClient(widget.focusNode.hasFocus); - } - - void _updateClient(bool hasFocus) { - if (hasFocus) { - if (Scribble.client != this) { - Scribble.client = this; - } - } else if (Scribble.client == this) { - Scribble.client = null; - } - } - @override void initState() { super.initState(); - _updateClient(widget.focusNode.hasFocus); - widget.focusNode.addListener(_onFocusChange); if (widget.enabled) { - Scribble.registerScribbleElement(elementIdentifier, this); + TextInput.registerScribbleElement(elementIdentifier, this); } } @override void didUpdateWidget(_ScribbleFocusable oldWidget) { super.didUpdateWidget(oldWidget); - if (oldWidget.focusNode != widget.focusNode) { - oldWidget.focusNode.removeListener(_onFocusChange); - widget.focusNode.addListener(_onFocusChange); - _updateClient(widget.focusNode.hasFocus); - } if (!oldWidget.enabled && widget.enabled) { - Scribble.registerScribbleElement(elementIdentifier, this); + TextInput.registerScribbleElement(elementIdentifier, this); } if (oldWidget.enabled && !widget.enabled) { - Scribble.unregisterScribbleElement(elementIdentifier); + TextInput.unregisterScribbleElement(elementIdentifier); } } @override void dispose() { - Scribble.unregisterScribbleElement(elementIdentifier); - widget.focusNode.removeListener(_onFocusChange); - if (Scribble.client == this) { - Scribble.client = null; - } + TextInput.unregisterScribbleElement(elementIdentifier); super.dispose(); } - // Start ScribbleClient. + RenderEditable? get renderEditable => widget.editableKey.currentContext?.findRenderObject() as RenderEditable?; static int _nextElementIdentifier = 1; final String _elementIdentifier; @@ -4579,38 +4539,15 @@ class _ScribbleFocusableState extends State<_ScribbleFocusable> with ScribbleCli @override void onScribbleFocus(Offset offset) { - return widget.onScribbleFocus(offset); - } - - @override - void insertTextPlaceholder(Size size) { - if (!widget.enabled || !widget.value.selection.isValid || widget.readOnly) { - return; - } - - widget.onPlaceholderLocationChanged( - widget.value.text.length - widget.value.selection.end, - ); - } - - @override - void removeTextPlaceholder() { - if (!widget.enabled) { - return; - } - - widget.onPlaceholderLocationChanged(-1); - } - - @override - void showToolbar() { - widget.onShowToolbar(); + widget.focusNode.requestFocus(); + renderEditable?.selectPositionAt(from: offset, cause: SelectionChangedCause.scribble); + widget.updateSelectionRects(); } @override bool isInScribbleRect(Rect rect) { final Rect calculatedBounds = bounds; - if (widget.readOnly) { + if (renderEditable?.readOnly ?? false) { return false; } if (calculatedBounds == Rect.zero) { @@ -4622,8 +4559,7 @@ class _ScribbleFocusableState extends State<_ScribbleFocusable> with ScribbleCli final Rect intersection = calculatedBounds.intersect(rect); final HitTestResult result = HitTestResult(); WidgetsBinding.instance.hitTest(result, intersection.center); - final RenderObject? renderObject = context.findRenderObject(); - return result.path.any((HitTestEntry entry) => entry.target == renderObject); + return result.path.any((HitTestEntry entry) => entry.target == renderEditable); } @override @@ -4636,8 +4572,6 @@ class _ScribbleFocusableState extends State<_ScribbleFocusable> with ScribbleCli return MatrixUtils.transformRect(transform, Rect.fromLTWH(0, 0, box.size.width, box.size.height)); } - // End ScribbleClient. - @override Widget build(BuildContext context) { return widget.child; diff --git a/packages/flutter/test/services/autofill_test.dart b/packages/flutter/test/services/autofill_test.dart index fb0bd612b0b..dba33c479ff 100644 --- a/packages/flutter/test/services/autofill_test.dart +++ b/packages/flutter/test/services/autofill_test.dart @@ -147,6 +147,21 @@ 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'; + } + @override void performSelector(String selectorName) { latestMethodCall = 'performSelector'; diff --git a/packages/flutter/test/services/binding_test.dart b/packages/flutter/test/services/binding_test.dart index dfc06b655d9..fbdd4e31fe0 100644 --- a/packages/flutter/test/services/binding_test.dart +++ b/packages/flutter/test/services/binding_test.dart @@ -106,4 +106,11 @@ void main() { await rootBundle.loadString('test_asset2'); expect(flutterAssetsCallCount, 4); }); + + test('initInstances sets a default method call handler for SystemChannels.textInput', () async { + final ByteData message = const JSONMessageCodec().encodeMessage({'method': 'TextInput.requestElementsInRect', 'args': null})!; + await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage('flutter/textinput', message, (ByteData? data) { + expect(data, isNotNull); + }); + }); } diff --git a/packages/flutter/test/services/delta_text_input_test.dart b/packages/flutter/test/services/delta_text_input_test.dart index 81b4c62fc60..4e98c5cd6b2 100644 --- a/packages/flutter/test/services/delta_text_input_test.dart +++ b/packages/flutter/test/services/delta_text_input_test.dart @@ -271,6 +271,21 @@ class FakeDeltaTextInputClient implements DeltaTextInputClient { latestMethodCall = 'showAutocorrectionPromptRect'; } + @override + void insertTextPlaceholder(Size size) { + latestMethodCall = 'insertTextPlaceholder'; + } + + @override + void removeTextPlaceholder() { + latestMethodCall = 'removeTextPlaceholder'; + } + + @override + void showToolbar() { + latestMethodCall = 'showToolbar'; + } + @override void performSelector(String selectorName) { latestMethodCall = 'performSelector'; diff --git a/packages/flutter/test/services/scribble_test.dart b/packages/flutter/test/services/scribble_test.dart deleted file mode 100644 index 0bc4fe6ef69..00000000000 --- a/packages/flutter/test/services/scribble_test.dart +++ /dev/null @@ -1,213 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'text_input_utils.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - test('ScribbleClient showToolbar method is called', () async { - final FakeScribbleElement targetElement = FakeScribbleElement(elementIdentifier: 'target'); - Scribble.client = targetElement; - - expect(targetElement.latestMethodCall, isEmpty); - - // Send showToolbar message. - final ByteData? messageBytes = - const JSONMessageCodec().encodeMessage({ - 'args': [1, 0, 1], - 'method': 'Scribble.showToolbar', - }); - await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage( - 'flutter/scribble', - messageBytes, - (ByteData? _) {}, - ); - - expect(targetElement.latestMethodCall, 'showToolbar'); - }); - - test('ScribbleClient removeTextPlaceholder method is called', () async { - final FakeScribbleElement targetElement = FakeScribbleElement(elementIdentifier: 'target'); - Scribble.client = targetElement; - - expect(targetElement.latestMethodCall, isEmpty); - - // Send removeTextPlaceholder message. - final ByteData? messageBytes = - const JSONMessageCodec().encodeMessage({ - 'args': [1, 0, 1], - 'method': 'Scribble.removeTextPlaceholder', - }); - await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage( - 'flutter/scribble', - messageBytes, - (ByteData? _) {}, - ); - - expect(targetElement.latestMethodCall, 'removeTextPlaceholder'); - }); - - test('ScribbleClient insertTextPlaceholder method is called', () async { - final FakeScribbleElement targetElement = FakeScribbleElement(elementIdentifier: 'target'); - Scribble.client = targetElement; - - expect(targetElement.latestMethodCall, isEmpty); - - // Send insertTextPlaceholder message. - final ByteData? messageBytes = - const JSONMessageCodec().encodeMessage({ - 'args': [1, 0, 1], - 'method': 'Scribble.insertTextPlaceholder', - }); - await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage( - 'flutter/scribble', - messageBytes, - (ByteData? _) {}, - ); - - expect(targetElement.latestMethodCall, 'insertTextPlaceholder'); - }); - - test('ScribbleClient scribbleInteractionBegan and scribbleInteractionFinished', () async { - Scribble.ensureInitialized(); - - expect(Scribble.scribbleInProgress, isFalse); - - // Send scribbleInteractionBegan message. - ByteData? messageBytes = - const JSONMessageCodec().encodeMessage({ - 'args': [1, 0, 1], - 'method': 'Scribble.scribbleInteractionBegan', - }); - await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage( - 'flutter/scribble', - messageBytes, - (ByteData? _) {}, - ); - - expect(Scribble.scribbleInProgress, isTrue); - - // Send scribbleInteractionFinished message. - messageBytes = - const JSONMessageCodec().encodeMessage({ - 'args': [1, 0, 1], - 'method': 'Scribble.scribbleInteractionFinished', - }); - await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage( - 'flutter/scribble', - messageBytes, - (ByteData? _) {}, - ); - - expect(Scribble.scribbleInProgress, isFalse); - }); - - test('ScribbleClient focusElement', () async { - final FakeScribbleElement targetElement = FakeScribbleElement(elementIdentifier: 'target'); - Scribble.registerScribbleElement(targetElement.elementIdentifier, targetElement); - final FakeScribbleElement otherElement = FakeScribbleElement(elementIdentifier: 'other'); - Scribble.registerScribbleElement(otherElement.elementIdentifier, otherElement); - - expect(targetElement.latestMethodCall, isEmpty); - expect(otherElement.latestMethodCall, isEmpty); - - // Send focusElement message. - final ByteData? messageBytes = - const JSONMessageCodec().encodeMessage({ - 'args': [targetElement.elementIdentifier, 0.0, 0.0], - 'method': 'Scribble.focusElement', - }); - await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage( - 'flutter/scribble', - messageBytes, - (ByteData? _) {}, - ); - - Scribble.unregisterScribbleElement(targetElement.elementIdentifier); - Scribble.unregisterScribbleElement(otherElement.elementIdentifier); - - expect(targetElement.latestMethodCall, 'onScribbleFocus'); - expect(otherElement.latestMethodCall, isEmpty); - }); - - test('ScribbleClient requestElementsInRect', () async { - final List targetElements = [ - 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 otherElements = [ - 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) => Scribble.registerScribbleElement(element.elementIdentifier, element); - void unregisterElements(FakeScribbleElement element) => Scribble.unregisterScribbleElement(element.elementIdentifier); - - [...targetElements, ...otherElements].forEach(registerElements); - - // Send requestElementsInRect message. - final ByteData? messageBytes = - const JSONMessageCodec().encodeMessage({ - 'args': [0.0, 50.0, 50.0, 100.0], - 'method': 'Scribble.requestElementsInRect', - }); - ByteData? responseBytes; - await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage( - 'flutter/scribble', - messageBytes, - (ByteData? response) { - responseBytes = response; - }, - ); - - [...targetElements, ...otherElements].forEach(unregisterElements); - - final List> responses = (const JSONMessageCodec().decodeMessage(responseBytes) as List).cast>(); - expect(responses.first.length, 2); - expect(responses.first.first, containsAllInOrder([targetElements.first.elementIdentifier, 0.0, 0.0, 100.0, 100.0])); - expect(responses.first.last, containsAllInOrder([targetElements.last.elementIdentifier, 0.0, 100.0, 100.0, 100.0])); - }); -} - -class FakeScribbleClient implements ScribbleClient { - FakeScribbleClient(); - - String latestMethodCall = ''; - - @override - String get elementIdentifier => ''; - - @override - void onScribbleFocus(Offset offset) { - latestMethodCall = 'onScribbleFocus'; - } - - @override - bool isInScribbleRect(Rect rect) { - latestMethodCall = 'isInScribbleRect'; - return false; - } - - @override - Rect get bounds => Rect.zero; - - @override - void showToolbar() { - latestMethodCall = 'showToolbar'; - } - - @override - void insertTextPlaceholder(Size size) { - latestMethodCall = 'insertTextPlaceholder'; - } - - @override - void removeTextPlaceholder() { - latestMethodCall = 'removeTextPlaceholder'; - } -} diff --git a/packages/flutter/test/services/text_input_test.dart b/packages/flutter/test/services/text_input_test.dart index 2e84cee168e..a1be567feb1 100644 --- a/packages/flutter/test/services/text_input_test.dart +++ b/packages/flutter/test/services/text_input_test.dart @@ -610,6 +610,148 @@ 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({ + 'args': [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({ + 'args': [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({ + 'args': [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({ + 'args': [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 targetElements = [ + 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 otherElements = [ + 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); + + [...targetElements, ...otherElements].forEach(registerElements); + + // Send requestElementsInRect message. + final ByteData? messageBytes = + const JSONMessageCodec().encodeMessage({ + 'args': [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; + }, + ); + + [...targetElements, ...otherElements].forEach(unregisterElements); + + final List> responses = (const JSONMessageCodec().decodeMessage(responseBytes) as List).cast>(); + expect(responses.first.length, 2); + expect(responses.first.first, containsAllInOrder([targetElements.first.elementIdentifier, 0.0, 0.0, 100.0, 100.0])); + expect(responses.first.last, containsAllInOrder([targetElements.last.elementIdentifier, 0.0, 100.0, 100.0, 100.0])); + }); }); test('TextEditingValue.isComposingRangeValid', () async { @@ -764,6 +906,12 @@ void main() { expect(fakeTextChannel.outgoingCalls.length, 6); expect(fakeTextChannel.outgoingCalls.last.method, 'TextInput.setEditableSizeAndTransform'); + connection.setSelectionRects(const [SelectionRect(position: 0, bounds: Rect.zero)]); + expectedMethodCalls.add('setSelectionRects'); + expect(control.methodCalls, expectedMethodCalls); + expect(fakeTextChannel.outgoingCalls.length, 7); + expect(fakeTextChannel.outgoingCalls.last.method, 'TextInput.setSelectionRects'); + connection.setStyle( fontFamily: null, fontSize: null, @@ -773,20 +921,20 @@ void main() { ); expectedMethodCalls.add('setStyle'); expect(control.methodCalls, expectedMethodCalls); - expect(fakeTextChannel.outgoingCalls.length, 7); + expect(fakeTextChannel.outgoingCalls.length, 8); expect(fakeTextChannel.outgoingCalls.last.method, 'TextInput.setStyle'); connection.close(); expectedMethodCalls.add('detach'); expect(control.methodCalls, expectedMethodCalls); - expect(fakeTextChannel.outgoingCalls.length, 8); + expect(fakeTextChannel.outgoingCalls.length, 9); expect(fakeTextChannel.outgoingCalls.last.method, 'TextInput.clearClient'); expectedMethodCalls.add('hide'); final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); await binding.runAsync(() async {}); await expectLater(control.methodCalls, expectedMethodCalls); - expect(fakeTextChannel.outgoingCalls.length, 9); + expect(fakeTextChannel.outgoingCalls.length, 10); expect(fakeTextChannel.outgoingCalls.last.method, 'TextInput.hide'); }); @@ -850,6 +998,11 @@ class FakeTextInputClient with TextInputClient { latestMethodCall = 'showAutocorrectionPromptRect'; } + @override + void showToolbar() { + latestMethodCall = 'showToolbar'; + } + TextInputConfiguration get configuration => const TextInputConfiguration(); @override @@ -857,6 +1010,16 @@ class FakeTextInputClient with TextInputClient { latestMethodCall = 'didChangeInputControl'; } + @override + void insertTextPlaceholder(Size size) { + latestMethodCall = 'insertTextPlaceholder'; + } + + @override + void removeTextPlaceholder() { + latestMethodCall = 'removeTextPlaceholder'; + } + @override void performSelector(String selectorName) { latestMethodCall = 'performSelector'; @@ -915,6 +1078,11 @@ class FakeTextInputControl with TextInputControl { methodCalls.add('setEditableSizeAndTransform'); } + @override + void setSelectionRects(List selectionRects) { + methodCalls.add('setSelectionRects'); + } + @override void setStyle({ required String? fontFamily, diff --git a/packages/flutter/test/services/text_input_utils.dart b/packages/flutter/test/services/text_input_utils.dart index e12cf3f50dc..67a89e69a91 100644 --- a/packages/flutter/test/services/text_input_utils.dart +++ b/packages/flutter/test/services/text_input_utils.dart @@ -65,7 +65,7 @@ class FakeTextChannel implements MethodChannel { } } -class FakeScribbleElement with ScribbleClient { +class FakeScribbleElement implements ScribbleClient { FakeScribbleElement({required String elementIdentifier, Rect bounds = Rect.zero}) : _elementIdentifier = elementIdentifier, _bounds = bounds; @@ -89,19 +89,4 @@ class FakeScribbleElement with ScribbleClient { void onScribbleFocus(Offset offset) { latestMethodCall = 'onScribbleFocus'; } - - @override - void insertTextPlaceholder(Size size) { - latestMethodCall = 'insertTextPlaceholder'; - } - - @override - void removeTextPlaceholder() { - latestMethodCall = 'removeTextPlaceholder'; - } - - @override - void showToolbar() { - latestMethodCall = 'showToolbar'; - } } diff --git a/packages/flutter/test/widgets/editable_text_test.dart b/packages/flutter/test/widgets/editable_text_test.dart index 51ebfa8e359..45e4484b2bd 100644 --- a/packages/flutter/test/widgets/editable_text_test.dart +++ b/packages/flutter/test/widgets/editable_text_test.dart @@ -2210,7 +2210,6 @@ void main() { final TextEditingController controller = TextEditingController(text: 'Lorem ipsum dolor sit amet'); late SelectionChangedCause selectionCause; - Scribble.ensureInitialized(); await tester.pumpWidget( MaterialApp( @@ -2230,7 +2229,7 @@ void main() { ), ); - await tester.testTextInput.scribbleFocusElement(Scribble.scribbleClients.keys.first, Offset.zero); + await tester.testTextInput.scribbleFocusElement(TextInput.scribbleClients.keys.first, Offset.zero); expect(focusNode.hasFocus, true); expect(selectionCause, SelectionChangedCause.scribble); @@ -2256,7 +2255,7 @@ void main() { ), ); - final List elementEntry = [Scribble.scribbleClients.keys.first, 0.0, 0.0, 800.0, 600.0]; + final List elementEntry = [TextInput.scribbleClients.keys.first, 0.0, 0.0, 800.0, 600.0]; List> elements = await tester.testTextInput.scribbleRequestElementsInRect(const Rect.fromLTWH(0, 0, 1, 1)); expect(elements.first, containsAll(elementEntry)); @@ -4630,8 +4629,8 @@ void main() { tester.binding.window.physicalSizeTestValue = const Size(750.0, 1334.0); final List> log = >[]; - SystemChannels.scribble.setMockMethodCallHandler((MethodCall methodCall) async { - if (methodCall.method == 'Scribble.setSelectionRects') { + SystemChannels.textInput.setMockMethodCallHandler((MethodCall methodCall) async { + if (methodCall.method == 'TextInput.setSelectionRects') { final List args = methodCall.arguments as List; final List selectionRects = []; for (final dynamic rect in args) { @@ -4802,76 +4801,6 @@ void main() { // 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.iOS })); // [intended] - testWidgets('scribble client is set based on most recent focus', (WidgetTester tester) async { - final List log = []; - SystemChannels.textInput.setMockMethodCallHandler((MethodCall methodCall) async { - log.add(methodCall); - }); - - final TextEditingController controller = TextEditingController(); - controller.text = 'Text1'; - - final GlobalKey key1 = GlobalKey(); - final GlobalKey key2 = GlobalKey(); - - final FocusNode focusNode1 = FocusNode(); - final FocusNode focusNode2 = FocusNode(); - - Scribble.client = null; - await tester.pumpWidget( - MediaQuery( - data: const MediaQueryData(), - child: Directionality( - textDirection: TextDirection.ltr, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - EditableText( - key: key1, - controller: TextEditingController(), - focusNode: focusNode1, - style: Typography.material2018().black.subtitle1!, - cursorColor: Colors.blue, - backgroundCursorColor: Colors.grey, - scribbleEnabled: false, - ), - EditableText( - key: key2, - controller: TextEditingController(), - focusNode: focusNode2, - style: Typography.material2018().black.subtitle1!, - cursorColor: Colors.blue, - backgroundCursorColor: Colors.grey, - scribbleEnabled: false, - ), - ], - ), - ), - ), - ); - - expect(Scribble.client, isNull); - - focusNode1.requestFocus(); - await tester.pump(); - - expect(Scribble.client, isNotNull); - final ScribbleClient client1 = Scribble.client!; - - focusNode2.requestFocus(); - await tester.pump(); - - expect(Scribble.client, isNot(client1)); - expect(Scribble.client, isNotNull); - - focusNode2.unfocus(); - await tester.pump(); - - expect(Scribble.client, isNull); - - // On web, we should rely on the browser's implementation of Scribble. - }, skip: kIsWeb); // [intended] - testWidgets('text styling info is sent on show keyboard', (WidgetTester tester) async { final List log = []; tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.textInput, (MethodCall methodCall) async { diff --git a/packages/flutter_test/lib/src/test_text_input.dart b/packages/flutter_test/lib/src/test_text_input.dart index 6735e48057a..55138cc94d4 100644 --- a/packages/flutter_test/lib/src/test_text_input.dart +++ b/packages/flutter_test/lib/src/test_text_input.dart @@ -285,10 +285,10 @@ class TestTextInput { Future startScribbleInteraction() async { assert(isRegistered); await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.handlePlatformMessage( - SystemChannels.scribble.name, - SystemChannels.scribble.codec.encodeMethodCall( + SystemChannels.textInput.name, + SystemChannels.textInput.codec.encodeMethodCall( MethodCall( - 'Scribble.scribbleInteractionBegan', + 'TextInputClient.scribbleInteractionBegan', [_client ?? -1,] ), ), @@ -300,10 +300,10 @@ class TestTextInput { Future scribbleFocusElement(String elementIdentifier, Offset offset) async { assert(isRegistered); await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.handlePlatformMessage( - SystemChannels.scribble.name, - SystemChannels.scribble.codec.encodeMethodCall( + SystemChannels.textInput.name, + SystemChannels.textInput.codec.encodeMethodCall( MethodCall( - 'Scribble.focusElement', + 'TextInputClient.focusElement', [elementIdentifier, offset.dx, offset.dy] ), ), @@ -316,15 +316,15 @@ class TestTextInput { assert(isRegistered); List> response = >[]; await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.handlePlatformMessage( - SystemChannels.scribble.name, - SystemChannels.scribble.codec.encodeMethodCall( + SystemChannels.textInput.name, + SystemChannels.textInput.codec.encodeMethodCall( MethodCall( - 'Scribble.requestElementsInRect', + 'TextInputClient.requestElementsInRect', [rect.left, rect.top, rect.width, rect.height] ), ), (ByteData? data) { - response = (SystemChannels.scribble.codec.decodeEnvelope(data!) as List).map((dynamic element) => element as List).toList(); + response = (SystemChannels.textInput.codec.decodeEnvelope(data!) as List).map((dynamic element) => element as List).toList(); }, ); @@ -335,10 +335,10 @@ class TestTextInput { Future scribbleInsertPlaceholder() async { assert(isRegistered); await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.handlePlatformMessage( - SystemChannels.scribble.name, - SystemChannels.scribble.codec.encodeMethodCall( + SystemChannels.textInput.name, + SystemChannels.textInput.codec.encodeMethodCall( MethodCall( - 'Scribble.insertTextPlaceholder', + 'TextInputClient.insertTextPlaceholder', [_client ?? -1, 0.0, 0.0] ), ), @@ -350,10 +350,10 @@ class TestTextInput { Future scribbleRemovePlaceholder() async { assert(isRegistered); await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.handlePlatformMessage( - SystemChannels.scribble.name, - SystemChannels.scribble.codec.encodeMethodCall( + SystemChannels.textInput.name, + SystemChannels.textInput.codec.encodeMethodCall( MethodCall( - 'Scribble.removeTextPlaceholder', + 'TextInputClient.removeTextPlaceholder', [_client ?? -1] ), ),