Unverified Commit 338841af authored by Xilai Zhang's avatar Xilai Zhang Committed by GitHub

Revert "Revert "Revert "Scribble mixin (#104128)" (#114647)" (#114698)" (#115146)

This reverts commit 3cde69e8.
parent 098aac7f
......@@ -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';
......
......@@ -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].
......
// 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<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 => Scribble._instance._scribbleClients;
/// Returns true if a scribble interaction is currently happening.
static bool get scribbleInProgress => _instance._scribbleInProgress;
Future<dynamic> _handleScribbleInvocation(MethodCall methodCall) async {
final String method = methodCall.method;
if (method == 'Scribble.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 == 'Scribble.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 == '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<dynamic> args = methodCall.arguments as List<dynamic>;
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<SelectionRect> _cachedSelectionRects = <SelectionRect>[];
/// 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<SelectionRect> selectionRects) {
if (!listEquals(_instance._cachedSelectionRects, selectionRects)) {
_instance._cachedSelectionRects = selectionRects;
_instance._channel.invokeMethod<void>(
'Scribble.setSelectionRects',
selectionRects.map((SelectionRect rect) {
return <num>[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)';
}
......@@ -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.
......
......@@ -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<SelectionRect> _cachedSelectionRects = <SelectionRect>[];
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<SelectionRect> 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<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> _loudlyHandleTextInputInvocation(MethodCall call) async {
try {
return await _handleTextInputInvocation(call);
......@@ -1668,8 +1764,33 @@ class TextInput {
rethrow;
}
}
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;
}
......@@ -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<SelectionRect> 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<SelectionRect> 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<SelectionRect> selectionRects) {
_channel.invokeMethod<void>(
'TextInput.setSelectionRects',
selectionRects.map((SelectionRect rect) {
return <num>[rect.bounds.left, rect.bounds.top, rect.bounds.width, rect.bounds.height, rect.position];
}).toList(),
);
}
@override
void setStyle({
required String? fontFamily,
......
......@@ -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';
......
......@@ -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(<String, dynamic>{'method': 'TextInput.requestElementsInRect', 'args': null})!;
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage('flutter/textinput', message, (ByteData? data) {
expect(data, isNotNull);
});
});
}
......@@ -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';
......
// 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(<String, dynamic>{
'args': <dynamic>[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(<String, dynamic>{
'args': <dynamic>[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(<String, dynamic>{
'args': <dynamic>[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(<String, dynamic>{
'args': <dynamic>[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(<String, dynamic>{
'args': <dynamic>[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(<String, dynamic>{
'args': <dynamic>[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<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) => Scribble.registerScribbleElement(element.elementIdentifier, element);
void unregisterElements(FakeScribbleElement element) => Scribble.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': 'Scribble.requestElementsInRect',
});
ByteData? responseBytes;
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
'flutter/scribble',
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]));
});
}
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';
}
}
......@@ -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(<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 {
......@@ -764,6 +906,12 @@ void main() {
expect(fakeTextChannel.outgoingCalls.length, 6);
expect(fakeTextChannel.outgoingCalls.last.method, 'TextInput.setEditableSizeAndTransform');
connection.setSelectionRects(const <SelectionRect>[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<SelectionRect> selectionRects) {
methodCalls.add('setSelectionRects');
}
@override
void setStyle({
required String? fontFamily,
......
......@@ -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';
}
}
......@@ -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<dynamic> elementEntry = <dynamic>[Scribble.scribbleClients.keys.first, 0.0, 0.0, 800.0, 600.0];
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));
......@@ -4630,8 +4629,8 @@ void main() {
tester.binding.window.physicalSizeTestValue = const Size(750.0, 1334.0);
final List<List<SelectionRect>> log = <List<SelectionRect>>[];
SystemChannels.scribble.setMockMethodCallHandler((MethodCall methodCall) async {
if (methodCall.method == 'Scribble.setSelectionRects') {
SystemChannels.textInput.setMockMethodCallHandler((MethodCall methodCall) async {
if (methodCall.method == 'TextInput.setSelectionRects') {
final List<dynamic> args = methodCall.arguments as List<dynamic>;
final List<SelectionRect> selectionRects = <SelectionRect>[];
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>{ TargetPlatform.iOS })); // [intended]
testWidgets('scribble client is set based on most recent focus', (WidgetTester tester) async {
final List<MethodCall> log = <MethodCall>[];
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: <Widget>[
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<MethodCall> log = <MethodCall>[];
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.textInput, (MethodCall methodCall) async {
......
......@@ -285,10 +285,10 @@ class TestTextInput {
Future<void> 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',
<dynamic>[_client ?? -1,]
),
),
......@@ -300,10 +300,10 @@ class TestTextInput {
Future<void> 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',
<dynamic>[elementIdentifier, offset.dx, offset.dy]
),
),
......@@ -316,15 +316,15 @@ class TestTextInput {
assert(isRegistered);
List<List<dynamic>> response = <List<dynamic>>[];
await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.handlePlatformMessage(
SystemChannels.scribble.name,
SystemChannels.scribble.codec.encodeMethodCall(
SystemChannels.textInput.name,
SystemChannels.textInput.codec.encodeMethodCall(
MethodCall(
'Scribble.requestElementsInRect',
'TextInputClient.requestElementsInRect',
<dynamic>[rect.left, rect.top, rect.width, rect.height]
),
),
(ByteData? data) {
response = (SystemChannels.scribble.codec.decodeEnvelope(data!) as List<dynamic>).map((dynamic element) => element as List<dynamic>).toList();
response = (SystemChannels.textInput.codec.decodeEnvelope(data!) as List<dynamic>).map((dynamic element) => element as List<dynamic>).toList();
},
);
......@@ -335,10 +335,10 @@ class TestTextInput {
Future<void> 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',
<dynamic>[_client ?? -1, 0.0, 0.0]
),
),
......@@ -350,10 +350,10 @@ class TestTextInput {
Future<void> 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',
<dynamic>[_client ?? -1]
),
),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment